Skip to content
4 changes: 4 additions & 0 deletions openwisp_controller/config/whois/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,8 @@ def process_ip_data_and_location(self, force_lookup=False):
Trigger WHOIS lookup based on the conditions of `_need_whois_lookup`.
Tasks are triggered on commit to ensure redundant data is not created.
"""
if self.device.is_deactivated():
return
Comment on lines +213 to +214
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Search for tests covering the deactivated device + WHOIS interaction

# Search for test methods that reference both deactivation and WHOIS
rg -nP 'def\s+test.*deactivat' --type=py -A 20 | rg -i 'whois|process_ip_data|update_whois'

# Search for tests that call the modified methods
rg -nP '(process_ip_data_and_location|update_whois_info)' --type=py --glob '*test*.py' -C 3

# Search for tests that mock or check is_deactivated in WHOIS context
rg -nP 'is_deactivated.*whois|whois.*is_deactivated' --type=py --glob '*test*.py' -i -C 3

Repository: openwisp/openwisp-controller

Length of output: 1490


🏁 Script executed:

# First, locate and examine the service.py file to understand the guard placement
fd -n 'service.py' openwisp_controller/config/whois/ -x cat -n {}

# Then search for tests covering deactivated devices in WHOIS context
rg -n 'is_deactivated' openwisp_controller/config/whois/tests/ --type=py -B 2 -A 10

# Search for test classes and methods related to WHOIS
rg -n 'class.*Test|def test_' openwisp_controller/config/whois/tests/tests.py --type=py | head -50

Repository: openwisp/openwisp-controller

Length of output: 2594


🏁 Script executed:

# View the service.py file around the mentioned lines
cat -n openwisp_controller/config/whois/service.py | sed -n '200,250p'

# Search for any reference to deactivated in the test file
rg -i 'deactivat' openwisp_controller/config/whois/tests/tests.py -n -B 2 -A 5

# Search for test methods that specifically test process_ip_data_and_location
rg -n 'def test.*process_ip_data|process_ip_data_and_location' openwisp_controller/config/whois/tests/tests.py -A 15

Repository: openwisp/openwisp-controller

Length of output: 4237


🏁 Script executed:

# Search for test methods that use deactivate()
rg -n 'deactivate()' openwisp_controller/config/whois/tests/tests.py -B 5 -A 20

# Check the test_whois_task_called test method to see if it covers deactivated devices
sed -n '476,540p' openwisp_controller/config/whois/tests/tests.py

# Search for any assertions about is_deactivated in tests
rg -n 'is_deactivated|deactivate' openwisp_controller/config/whois/tests/tests.py -A 10

Repository: openwisp/openwisp-controller

Length of output: 6499


🏁 Script executed:

# Get the full test_whois_update method
sed -n '741,820p' openwisp_controller/config/whois/tests/tests.py

# Search for any test that explicitly tests behavior with deactivated devices and WHOIS methods
rg -n 'def test.*' openwisp_controller/config/whois/tests/tests.py | xargs -I {} bash -c "grep -q 'is_deactivated\|deactivate()' openwisp_controller/config/whois/tests/tests.py && echo {}"

Repository: openwisp/openwisp-controller

Length of output: 11670


🏁 Script executed:

# Final verification: confirm no other tests cover deactivated + WHOIS interaction
rg -n 'process_ip_data_and_location|update_whois_info' openwisp_controller/config/whois/tests/tests.py -B 3 -A 3

Repository: openwisp/openwisp-controller

Length of output: 818


Add test coverage for deactivated device scenarios.

Tests are missing to verify:

  1. Both process_ip_data_and_location and update_whois_info skip processing when device.is_deactivated() is True
  2. No WHOIS tasks are scheduled for deactivated devices
  3. No whois_lookup_skipped signal is emitted for deactivated devices

The PR checklist indicates tests are not yet completed. These guards prevent critical side effects (task scheduling and signal emission) and require test coverage to prevent regressions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openwisp_controller/config/whois/service.py` around lines 213 - 214, Add unit
tests that assert both process_ip_data_and_location and update_whois_info return
early when device.is_deactivated() is True: mock a device with is_deactivated()
-> True, call Service.process_ip_data_and_location and
Service.update_whois_info, and assert they do not perform work. Specifically
verify that no WHOIS Celery task is scheduled (mock the task scheduler or
delay/apply_async) and that the whois_lookup_skipped signal is not emitted
(connect a test receiver or mock the signal send) for deactivated devices;
reference the Service methods process_ip_data_and_location and update_whois_info
and the whois_lookup_skipped signal to locate the logic to test. Ensure tests
cover both methods and assert absence of task scheduling and signal emission.

new_ip = self.device.last_ip
initial_ip = self.device._initial_last_ip
if force_lookup or self._need_whois_lookup(new_ip):
Expand All @@ -229,6 +231,8 @@ def update_whois_info(self):
when the data is older than
``OPENWISP_CONTROLLER_WHOIS_REFRESH_THRESHOLD_DAYS``.
"""
if self.device.is_deactivated():
return
ip_address = self.device.last_ip
if not self.is_valid_public_ip_address(ip_address):
return
Expand Down
76 changes: 75 additions & 1 deletion openwisp_controller/config/whois/tests/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from datetime import timedelta
from io import StringIO
from unittest import mock
from unittest.mock import patch
from uuid import uuid4

from django.conf import settings
Expand All @@ -21,7 +22,22 @@
from selenium.webdriver.common.by import By
from swapper import load_model

<<<<<<< HEAD
from openwisp_controller.config.models import Device
from openwisp_controller.config.signals import whois_fetched, whois_lookup_skipped
from openwisp_controller.config.whois.service import WhoIsService
Comment on lines +26 to +28
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the actual service class name exported from whois.service
rg -nP '^\s*class\s+\w*[Ww]ho[Ii][Ss]\w*\s*[:\(]' --type=py
# And check what's importable
rg -nP '\bfrom\s+openwisp_controller\.config\.whois\.service\s+import\b' --type=py

Repository: openwisp/openwisp-controller

Length of output: 279


🏁 Script executed:

# Check the actual service file for class definitions
cat openwisp_controller/config/whois/service.py | head -50

# Also search for any class definition with WHOIS in the name
rg -n 'class.*[Ww]ho' openwisp_controller/config/whois/service.py

Repository: openwisp/openwisp-controller

Length of output: 1569


🏁 Script executed:

# Check the test file imports and Device assignment
sed -n '1,50p' openwisp_controller/config/whois/tests/tests.py

Repository: openwisp/openwisp-controller

Length of output: 2099


🏁 Script executed:

# Check TestWhoisDeactivated class implementation
sed -n '1194,1230p' openwisp_controller/config/whois/tests/tests.py

# Check lines 350-355 for stale comment
sed -n '350,360p' openwisp_controller/config/whois/tests/tests.py

# Check if WhoIsService is used anywhere in the test file
rg -n 'WhoIsService' openwisp_controller/config/whois/tests/tests.py

Repository: openwisp/openwisp-controller

Length of output: 1540


🏁 Script executed:

# Check Device model to see required fields
rg -A 20 'class Device' openwisp_controller/config/models.py | head -50

# Search for organization field requirement in Device
rg -n 'organization.*required|organization.*null.*False' openwisp_controller/config/models.py | head -20

# Check lines 350-360 more carefully for the stale comment
sed -n '348,365p' openwisp_controller/config/whois/tests/tests.py

Repository: openwisp/openwisp-controller

Length of output: 1678


🏁 Script executed:

# Find AbstractDevice and its required fields
rg -A 50 'class AbstractDevice' openwisp_controller/config/models.py | head -80

# Check if organization is a required FK field
rg -n 'organization.*ForeignKey|organization.*models\.' openwisp_controller/config/models.py | head -10

Repository: openwisp/openwisp-controller

Length of output: 54


🏁 Script executed:

# Search for AbstractDevice in abstract_models or base files
fd -t f AbstractDevice openwisp_controller/

# Find where AbstractDevice is defined
rg -l 'class AbstractDevice' --type=py

# Get device creation pattern from test utils
rg -B2 -A5 '_create_device' openwisp_controller/config/whois/tests/tests.py | head -40

Repository: openwisp/openwisp-controller

Length of output: 1576


🏁 Script executed:

# Find the _create_device method definition in test utils
rg -B5 -A15 'def _create_device' openwisp_controller/config/whois/tests/tests.py | head -40

# Also check the CreateWHOISMixin that's imported
rg -B5 -A15 'def _create_device' openwisp_controller/config/whois/tests/utils.py

Repository: openwisp/openwisp-controller

Length of output: 54


🏁 Script executed:

# Check the test file imports to understand the inheritance chain
grep -n 'class.*TestCase\|CreateWHOISMixin\|TestAdminMixin' openwisp_controller/config/whois/tests/tests.py | head -20

# Check CreateWHOISMixin definition
rg -A20 'class CreateWHOISMixin' openwisp_controller/config/whois/tests/utils.py

# Also check TestAdminMixin
fd -t f 'TestAdminMixin' openwisp_controller/

Repository: openwisp/openwisp-controller

Length of output: 1177


🏁 Script executed:

# Find CreateConfigMixin to see what _create_device does
rg -B5 -A15 'class CreateConfigMixin\|def _create_device' openwisp_controller/config/tests/utils.py | head -60

# Check AbstractDevice to see required fields
sed -n '1,100p' openwisp_controller/config/base/device.py | grep -A5 -B5 'organization\|mac_address'

Repository: openwisp/openwisp-controller

Length of output: 1040


🏁 Script executed:

# Check OrgMixin to see organization field requirements
rg -B5 -A20 'class OrgMixin' openwisp_controller/config/base/device.py

# Check CreateConfigMixin to see _create_device implementation
rg -B2 -A20 'class CreateConfigMixin\|def _create_device' openwisp_controller/config/tests/utils.py | head -80

Repository: openwisp/openwisp-controller

Length of output: 54


🏁 Script executed:

# Search for OrgMixin definition
rg -l 'class OrgMixin' --type=py

# Check the CreateConfigMixin file directly
head -150 openwisp_controller/config/tests/utils.py | tail -100

# Search for _create_device method
rg 'def _create_device' -A15 openwisp_controller/config/tests/utils.py

Repository: openwisp/openwisp-controller

Length of output: 4846


🏁 Script executed:

# Confirm the stale comment - check lines 350-365 again more carefully
sed -n '347,365p' openwisp_controller/config/whois/tests/tests.py

Repository: openwisp/openwisp-controller

Length of output: 1014


🏁 Script executed:

# Check if TestWhoisDeactivated has access to _get_org or _get_org() method
rg -B5 'class TestWhoisDeactivated' openwisp_controller/config/whois/tests/tests.py

# Check if any test uses Device.objects.create without organization
rg 'Device\.objects\.create.*name=' openwisp_controller/config/whois/tests/tests.py | head -20

# Verify that Device requires organization by checking model constraints
rg 'organization.*null.*False|null.*False.*organization' openwisp_controller/config/base/device.py | head -10

# Check if mac_address is required
rg 'mac_address.*blank.*False|mac_address.*required|blank=False.*mac_address' openwisp_controller/config/base/device.py

Repository: openwisp/openwisp-controller

Length of output: 351


🏁 Script executed:

# Check the Device model field definitions directly
sed -n '1,200p' openwisp_controller/config/base/device.py | grep -A3 'organization\|mac_address' | head -40

# Check if organization is inherited from OrgMixin
rg -B5 -A15 'class OrgMixin' --type=py

# Try to find where OrgMixin is defined
fd -t f '*.py' | xargs grep -l 'class OrgMixin' | head -5

Repository: openwisp/openwisp-controller

Length of output: 1625


🏁 Script executed:

# Find where OrgMixin is defined
rg -l 'class OrgMixin' --type=py

# Check if Device.objects.create() call in TestWhoisDeactivated will fail
# First, find if organization field is null=True
rg 'organization.*ForeignKey' openwisp_controller/config/base/ -A3 | head -20

# Count how many tests use _create_device vs Device.objects.create directly
rg 'Device\.objects\.create' openwisp_controller/config/whois/tests/tests.py

Repository: openwisp/openwisp-controller

Length of output: 179


🏁 Script executed:

# Find OrgMixin definition
rg -B2 -A25 'class OrgMixin' --type=py | head -60

# Check if organization field has null=False or blank=False
rg 'organization.*models\.ForeignKey' openwisp_controller/ -A5 | head -30

Repository: openwisp/openwisp-controller

Length of output: 54


Fix broken import: WhoIsService class does not exist; use WHOISService already imported on line 33.

Line 27 attempts to import WhoIsService, but the actual class name is WHOISService (defined in openwisp_controller/config/whois/service.py). This causes an ImportError at test collection time. Remove the incorrect import on line 27 and use the correct WHOISService already imported on line 33 in the test class.

Additionally:

  • Remove the dead import of Device on line 25 (immediately shadowed by load_model("config", "Device") on line 39).
  • Line 1196 in TestWhoisDeactivated.setUp(): Device.objects.create(name="test-device") is missing required fields organization and mac_address. Use self._create_device() inherited from the test mixin, or pass these required parameters.
  • Line 352 comment states "4 queries" but the assertion expects 7 queries; update the comment to match.
Suggested fix
-from openwisp_controller.config.models import Device
 from openwisp_controller.config.signals import whois_fetched, whois_lookup_skipped
-from openwisp_controller.config.whois.service import WhoIsService
 from openwisp_utils.tests import SeleniumTestMixin, catch_signal

Update TestWhoisDeactivated to use WHOISService and fix device creation to include required fields.

Update line 352 comment to "7 queries" instead of "4 queries".

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
from openwisp_controller.config.models import Device
from openwisp_controller.config.signals import whois_fetched, whois_lookup_skipped
from openwisp_controller.config.whois.service import WhoIsService
from openwisp_controller.config.signals import whois_fetched, whois_lookup_skipped
from openwisp_utils.tests import SeleniumTestMixin, catch_signal
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openwisp_controller/config/whois/tests/tests.py` around lines 25 - 27, Remove
the incorrect import of WhoIsService and the dead import of Device; ensure the
tests reference the existing WHOISService class (use WHOISService where
WhoIsService was used) and rely on load_model("config", "Device") already used
in the file. In TestWhoisDeactivated.setUp(), replace the direct
Device.objects.create(name="test-device") call with the mixin helper
self._create_device() or supply the required organization and mac_address fields
so creation succeeds. Finally, update the inline comment that currently says "4
queries" to match the test assertion (change it to "7 queries") so comment and
expectation align.

=======
<<<<<<< HEAD
from openwisp_controller.config import settings as app_settings
from openwisp_controller.config.models import Device
from openwisp_controller.config.signals import whois_fetched, whois_lookup_skipped
from openwisp_controller.config.whois.service import WHOISService
=======
from openwisp_controller.config.models import Device
from openwisp_controller.config.signals import whois_fetched, whois_lookup_skipped
from openwisp_controller.config.whois.service import WhoIsService
>>>>>>> 8d798b46448620c8536e84519b69134dc556bd56
>>>>>>> 38cb7ad (test(geo): add regression tests for deactivated device WHOIS handling)
from openwisp_utils.tests import SeleniumTestMixin, catch_signal

from ....tests.utils import TestAdminMixin
Expand Down Expand Up @@ -346,8 +362,15 @@ def test_last_ip_management_command_queries(self):
name="default.test.device4", mac_address="66:33:44:55:66:77"
)
args = ["--noinput"]
<<<<<<< HEAD

=======
# 4 queries (3 for each device's last_ip update) and 1 for fetching devices
with self.assertNumQueries(4):
<<<<<<< HEAD
=======
>>>>>>> 8d798b46448620c8536e84519b69134dc556bd56
>>>>>>> 38cb7ad (test(geo): add regression tests for deactivated device WHOIS handling)
with self.assertNumQueries(7):
Comment thread
coderabbitai[bot] marked this conversation as resolved.
call_command("clear_last_ip", *args, stdout=out, stderr=StringIO())

@mock.patch.object(app_settings, "WHOIS_CONFIGURED", True)
Expand Down Expand Up @@ -1186,3 +1209,54 @@ def _assert_no_js_errors():
_assert_no_js_errors()
except UnexpectedAlertPresentException:
self.fail("XSS vulnerability detected in WHOIS details admin view.")


<<<<<<< HEAD
class TestWhoisDeactivated(TestCase):
def setUp(self):
self.device = Device.objects.create(name="test-device")
Comment on lines +1216 to +1217
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Device.objects.create(name="test-device") will fail validation.

Device requires at minimum organization and mac_address. Even if you bypass full_clean, the FK to organization is non-nullable and creation will raise IntegrityError. Use self._create_device(...) from CreateWHOISMixin / TestOrganizationMixin as every other test in this file does.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openwisp_controller/config/whois/tests/tests.py` around lines 1195 - 1196,
The setUp currently calls Device.objects.create(name="test-device") which fails
because Device requires organization and mac_address; replace that call with the
test helper self._create_device(...) provided by
CreateWHOISMixin/TestOrganizationMixin (pass at least a name and allow the
helper to populate organization and mac_address) so setUp uses
self._create_device("test-device") (or equivalent arguments) instead of
Device.objects.create to avoid IntegrityError and follow the pattern used by
other tests.


@patch("openwisp_controller.config.whois.service.fetch_whois_details.delay")
def test_process_ip_skips_when_deactivated(self, mock_task):
self.device._is_deactivated = True

service = WhoIsService(self.device)
service.process_ip_data_and_location()

mock_task.assert_not_called()

@patch("openwisp_controller.config.whois.service.fetch_whois_details.delay")
def test_update_whois_skips_when_deactivated(self, mock_task):
self.device._is_deactivated = True

service = WhoIsService(self.device)
service.update_whois_info()

mock_task.assert_not_called()
Comment thread
coderabbitai[bot] marked this conversation as resolved.
=======
class TestWHOISDeactivated(TransactionTestCase):
def setUp(self):
self.device = self._create_device()
self.device.last_ip = "8.8.8.8" # public IP
self.device.save()

@mock.patch.object(app_settings, "WHOIS_CONFIGURED", True)
@mock.patch("openwisp_controller.config.whois.service.fetch_whois_details.delay")
def test_process_ip_skips_when_deactivated(self, mock_task):
self.device._is_deactivated = True

service = WHOISService(self.device)
service.process_ip_data_and_location()

self.assertEqual(mock_task.call_count, 0)

@mock.patch.object(app_settings, "WHOIS_CONFIGURED", True)
@mock.patch("openwisp_controller.config.whois.service.fetch_whois_details.delay")
def test_process_ip_runs_when_active(self, mock_task):
self.device._is_deactivated = False

service = WHOISService(self.device)
service.process_ip_data_and_location()

self.assertEqual(mock_task.call_count, 1)
>>>>>>> 38cb7ad (test(geo): add regression tests for deactivated device WHOIS handling)
Loading