@@ -311,7 +311,12 @@ class UtilsModulePathFalseTest(TestCase):
311311 """Test supports_module_path() returns False when MODULE_PATH_TOKEN is missing."""
312312
313313 def test_returns_false_when_token_missing (self ):
314- """supports_module_path() catches ImportError and returns False (lines 16-17)."""
314+ """supports_module_path() returns False when MODULE_PATH_TOKEN is absent.
315+
316+ Temporarily removes the attribute from dcim.constants (if present) to
317+ trigger the ImportError branch in supports_module_path, then restores it.
318+ Asserts False regardless of whether the attribute existed beforehand.
319+ """
315320 import dcim .constants as dc
316321
317322 from netbox_interface_name_rules .utils import supports_module_path
@@ -322,11 +327,7 @@ def test_returns_false_when_token_missing(self):
322327 if had_attr :
323328 delattr (dc , "MODULE_PATH_TOKEN" )
324329 result = supports_module_path ()
325- if had_attr :
326- self .assertFalse (result )
327- else :
328- # Already False before this test; just verify the type
329- self .assertIsInstance (result , bool )
330+ self .assertFalse (result )
330331 finally :
331332 if had_attr :
332333 dc .MODULE_PATH_TOKEN = original
@@ -441,22 +442,32 @@ def _toggle_url(self, pk):
441442 )
442443
443444 def test_toggle_non_ajax_no_permission_raises_403 (self ):
444- """Non-AJAX POST from user without change permission returns 403 (lines 430-432)."""
445+ """Non-AJAX POST from user without change permission returns 403.
446+
447+ NetBox's ObjectView dispatch checks permissions via LoginRequiredMixin and
448+ raises PermissionDenied when the user lacks change_interfacenamerule, which
449+ Django converts to a 403 response.
450+ """
445451 User .objects .create_user (username = "noperm_tog_user2" , password = "testpass123" )
446452 self .client .login (username = "noperm_tog_user2" , password = "testpass123" )
447453 url = self ._toggle_url (self .rule .pk )
448454 response = self .client .post (url )
449- self .assertIn (response .status_code , [ 403 , 302 ] )
455+ self .assertEqual (response .status_code , 403 )
450456
451457 def test_post_apply_apply_error_shows_error_message (self ):
452- """POST apply when apply_rule_to_existing raises shows error message (lines 413-415)."""
458+ """POST apply when apply_rule_to_existing raises shows error message and redirects.
459+
460+ Patches engine.apply_rule_to_existing to raise ValueError, verifying the
461+ view catches the exception (lines 413-415), logs it, adds an error message,
462+ and still issues the 302 redirect back to the apply detail page.
463+ """
453464 url = reverse (
454465 "plugins:netbox_interface_name_rules:interfacenamerule_apply_detail" ,
455466 kwargs = {"pk" : self .rule .pk },
456467 )
457468 with patch ("netbox_interface_name_rules.engine.apply_rule_to_existing" , side_effect = ValueError ("bad template" )):
458469 response = self .client .post (url , {"action" : "apply" , "interface_ids" : [str (self .rule .pk )]})
459- self .assertIn (response .status_code , [ 302 ] )
470+ self .assertEqual (response .status_code , 302 )
460471
461472 def test_apply_detail_get_value_error_shows_error (self ):
462473 """GET apply detail when find_interfaces_for_rule raises ValueError shows error (lines 357-360)."""
@@ -638,7 +649,13 @@ def setUpTestData(cls):
638649 cls .bad_regex_rule .save ()
639650
640651 def test_invalid_regex_pattern_is_skipped_not_raised (self ):
641- """_find_regex_match silently skips rules with bad regex (lines 355-356)."""
652+ """_find_regex_match silently skips rules whose module_type_pattern is not valid regex.
653+
654+ The bad-pattern rule is inserted directly via save() to bypass clean()
655+ validation. _find_regex_match wraps re.fullmatch in a try/except re.error
656+ so an invalid pattern just moves on to the next candidate instead of
657+ propagating the exception to the caller (lines 355-356).
658+ """
642659 from netbox_interface_name_rules .engine import _find_regex_match
643660
644661 candidates = [(None , None , None )]
@@ -897,7 +914,11 @@ def _url(self):
897914 return reverse ("plugins:netbox_interface_name_rules:interfacenamerule_test" )
898915
899916 def test_save_rule_with_device_type_filters_scope (self ):
900- """POST save_rule with device_type set applies scope filter (line 203)."""
917+ """POST save_rule with device_type set applies scope filter and redirects.
918+
919+ Verifies that when both module_type and device_type are provided, the view
920+ looks up an existing matching rule (line 203) and redirects to it (302).
921+ """
901922 data = {
902923 "name_template" : "et-0/0/{bay_position}" ,
903924 "channel_count" : "0" ,
@@ -907,8 +928,7 @@ def test_save_rule_with_device_type_filters_scope(self):
907928 "action" : "save_rule" ,
908929 }
909930 response = self .client .post (self ._url (), data )
910- # Should redirect (either to existing rule or to add page)
911- self .assertIn (response .status_code , [302 ])
931+ self .assertEqual (response .status_code , 302 )
912932
913933 def test_save_rule_no_match_with_device_type_redirects_to_add_with_pk (self ):
914934 """POST save_rule with unmatched scope redirects to add page with device_type param (line 227)."""
@@ -1002,7 +1022,7 @@ def test_post_background_success_shows_success_message(self):
10021022 mock_job .pk = 42
10031023 with patch ("netbox_interface_name_rules.jobs.ApplyRuleJob.enqueue" , return_value = mock_job ):
10041024 response = self .client .post (url , {"action" : "background" })
1005- self .assertIn (response .status_code , [ 302 ] )
1025+ self .assertEqual (response .status_code , 302 )
10061026
10071027
10081028# ---------------------------------------------------------------------------
@@ -1035,7 +1055,15 @@ def setUpTestData(cls):
10351055 cls .bay = ModuleBay .objects .get (device = cls .device , name = "NBBay 0" )
10361056
10371057 def test_module_with_null_bay_is_skipped (self ):
1038- """_apply_rules_for_device_deferred skips module when module_bay is None (line 214)."""
1058+ """_apply_rules_for_device_deferred continues past modules with module_bay=None.
1059+
1060+ The signal function iterates over all modules on a device and calls
1061+ apply_interface_name_rules for each one. When module_bay is None (e.g. due
1062+ to a data inconsistency), the loop must skip that entry via ``continue``
1063+ rather than passing None to the engine. A FakeModule with module_bay=None
1064+ is injected via a patch on Module.objects.filter so the DB doesn't need
1065+ to hold inconsistent data.
1066+ """
10391067 from netbox_interface_name_rules .signals import _apply_rules_for_device_deferred
10401068
10411069 module = Module .objects .create (device = self .device , module_bay = self .bay , module_type = self .module_type )
@@ -1077,7 +1105,13 @@ def setUpTestData(cls):
10771105 cls .bay = ModuleBay .objects .get (device = cls .device , name = "CRBay 0" )
10781106
10791107 def test_valueerror_in_template_sets_error_name (self ):
1080- """_channel_rule_entry uses error name when template raises ValueError (lines 618-619)."""
1108+ """_channel_rule_entry stores an ``<error: …>`` placeholder when evaluate_name_template raises.
1109+
1110+ evaluate_name_template is patched to raise ValueError for every call so
1111+ the loop that builds expected_names catches it and collapses to a single
1112+ error-placeholder entry (lines 618-619). The result dict is then returned
1113+ because the placeholder is not in existing_names.
1114+ """
10811115 from netbox_interface_name_rules .engine import _channel_rule_entry
10821116
10831117 rule = InterfaceNameRule (
@@ -1147,7 +1181,13 @@ def setUpTestData(cls):
11471181 cls .bay = ModuleBay .objects .get (device = cls .device , name = "CLBay 0" )
11481182
11491183 def test_limit_reached_returns_true (self ):
1150- """_process_channel_module returns should_stop=True when limit is reached (line 645)."""
1184+ """_process_channel_module returns should_stop=True when the result limit is hit.
1185+
1186+ ``results`` is pre-seeded with one entry so that after _channel_rule_entry
1187+ adds a new entry the total reaches ``limit=1``, triggering the early-stop
1188+ path (line 644-645). The module_qs mock with count=0 ensures no extra
1189+ work is counted from the remaining queryset.
1190+ """
11511191 from netbox_interface_name_rules .engine import _process_channel_module
11521192
11531193 rule = InterfaceNameRule .objects .create (
0 commit comments