Skip to content

Feat/bulk import - #115

Merged
marcinpsk merged 103 commits into
developfrom
feat/bulk-import
Aug 10, 2026
Merged

Feat/bulk import#115
marcinpsk merged 103 commits into
developfrom
feat/bulk-import

Conversation

@marcinpsk

@marcinpsk marcinpsk commented Jun 19, 2026

Copy link
Copy Markdown
Owner

Summary

Stacked on #114 — review only the delta over feat/device-merge.

Bulk-import collision detection. At refresh time each import row is re-checked against existing NetBox devices by serial, primary IP and oob_ip before it can be imported, so a device that already exists under a different name can't be re-imported as new.

Motivation / Problem

Feature / bug. Close a gap where a row whose librenms_id/name link disappeared could flip to importable and duplicate an existing device.

Scope of Change

  • Sync/Import logic
  • Web UI / templates
  • Tests

How Was This Tested?

  • Unit tests: yes — refresh re-match by serial / primary IP / oob_ip blocks import; real Device/Interface/IP rows.

Risk Assessment

Makes the refresh re-check stricter (fail-closed); cannot cause new imports.

Backwards Compatibility

  • No breaking changes

Summary by CodeRabbit

  • New Features

    • Added bulk-import collision detection across devices and virtual machines.
    • Added collision review screens showing affected NetBox records and LibreNMS entries.
    • Improved merge and promotion forms with server-specific actions and field handling.
  • Bug Fixes

    • Improved handling of unresolved, ambiguous, cancelled, and conflicting imports.
    • Fixed cache validation, stale-data refresh, server scoping, pagination, and VLAN synchronization concurrency.
    • Strengthened object-level permission checks across synchronization and import actions.
    • Restricted actions for conflicts involving hidden modules.
  • Tests

    • Expanded coverage for imports, collisions, permissions, caching, validation, and synchronization.

@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds bulk collision prechecks, fail-closed cache validation, dynamic import permissions, permission-scoped synchronization lookups, linkage refresh handling, and extensive database-backed regression coverage.

Changes

Import validation and synchronization safeguards

Layer / File(s) Summary
Bulk validation and collision gates
netbox_librenms_plugin/import_utils/*, netbox_librenms_plugin/import_validation_helpers.py, netbox_librenms_plugin/jobs.py
Bulk imports validate identities, detect device and VM collisions, handle cancellation, classify unresolved rows, and apply per-row permission checks.
Permission-scoped synchronization
netbox_librenms_plugin/views/mixins.py, netbox_librenms_plugin/views/sync/*, netbox_librenms_plugin/views/object_sync/devices.py, netbox_librenms_plugin/views/imports/actions.py
Object reads and locks use restricted querysets. Synchronization flows enforce parent-object visibility and target write permissions.
Shared state and rendering fixes
netbox_librenms_plugin/utils.py, netbox_librenms_plugin/views/base/*, netbox_librenms_plugin/templates/*
Shared helpers validate row identity, normalize server keys and model text, and update collision and hidden-conflict displays.
Regression and integration coverage
netbox_librenms_plugin/tests/*
Tests cover collisions, cache handling, permissions, cancellation, concurrency, templates, migration markers, numeric values, and real request/view behavior.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ImportView
  participant NetBox
  participant Precheck
  participant LibreNMS
  participant ImportJob
  ImportView->>NetBox: Check import permissions
  ImportView->>Precheck: Scan selected device and VM rows
  Precheck->>LibreNMS: Reuse verified cache or fetch device data
  Precheck->>NetBox: Validate targets and detect collisions
  Precheck-->>ImportView: Return approved, unresolved, or blocked rows
  ImportView->>ImportJob: Dispatch approved rows
  ImportJob-->>ImportView: Record imports and row failures
Loading

Possibly related PRs

Poem

A rabbit checks each import row,
Verified keys tell it where to go.
Collisions close the gateway tight,
Scoped reads keep targets right.
Safe rows hop to work. 🐰

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.79% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description check ✅ Passed The description covers the summary, motivation, scope, testing, risk, and backward compatibility requirements.
Title check ✅ Passed The title identifies the bulk-import feature, which is the main area of change, although it does not mention collision detection.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/bulk-import
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch feat/bulk-import

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@marcinpsk
marcinpsk force-pushed the feat/bulk-import branch 5 times, most recently from 432f566 to aece92b Compare June 19, 2026 17:58
@marcinpsk

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 20, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
netbox_librenms_plugin/tests/test_coverage_sync_views2.py (1)

1257-1282: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Make this VM interface test fail if the port-id lookup regresses.

interface_name is still "eth0", so the test passes even if _match_interface() ignores port_id and falls back by name. Use a deliberately stale/non-matching name while keeping port_id=9 so this assertion proves the LibreNMS ID path is used. Based on learnings, MagicMock setups should not return the same result for both the librenms_id lookup and name fallback when the test is meant to distinguish those paths.

🧪 Proposed hardening
 ip_data = {
     "ip_address": "10.0.0.5",
     "ip_with_mask": "10.0.0.5/24",
     "port_id": 9,
-    "interface_name": "eth0",
+    # Deliberately stale: the match must come from port_id, not name fallback.
+    "interface_name": "cached-old-name",
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@netbox_librenms_plugin/tests/test_coverage_sync_views2.py` around lines 1257
- 1282, The test currently uses interface_name "eth0" which could match by name
fallback even if the port_id lookup is ignored, making the test unable to
distinguish between the librenms_id path and name fallback path. Change the
interface_name value in the ip_data dictionary to a deliberately stale or
non-matching name (different from "eth0") while keeping port_id=9 unchanged.
This ensures the test will only pass if the _match_interface() function
correctly uses the port_id lookup and does not regress to relying solely on name
matching.

Source: Learnings

netbox_librenms_plugin/import_utils/bulk_import.py (1)

590-637: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Prefer any librenms_id hit before name fallbacks across models.

The guard detects cross_id_match, but _lookup_in_model(Model) still tries preferred-model name matches before CrossModel is considered. If the scanned LibreNMS ID is already linked to a VM while a Device happens to share the resolved/hostname, refresh will bind the Device by name instead of the ID-linked VM, disagreeing with validation and potentially rendering actions for the wrong object.

🐛 Proposed fix to make ID matches win across both models
-        def _lookup_in_model(m):
-            """Return (device, match_type) for model m, or (None, None)."""
-            if librenms_id is not None:
-                dev = find_by_librenms_id(m, librenms_id, server_key)
-                if dev:
-                    return dev, "librenms_id"
+        def _lookup_by_name_in_model(m):
+            """Return (device, match_type) for name-based matches in model m, or (None, None)."""
             resolved_name = validation.get("resolved_name")
             if resolved_name:
                 dev = m.objects.filter(name__iexact=resolved_name).first()
                 if dev:
                     return dev, "resolved_name"
@@
         if librenms_id is not None:
             model_id_match = find_by_librenms_id(Model, librenms_id, server_key)
             cross_id_match = find_by_librenms_id(CrossModel, librenms_id, server_key)
             if model_id_match and cross_id_match:
                 raise AmbiguousLibreNMSIdError(
                     f"LibreNMS ID {librenms_id} matches both {Model.__name__} and {CrossModel.__name__}"
                 )
+            if model_id_match:
+                new_device, match_type = model_id_match, "librenms_id"
+            elif cross_id_match:
+                new_device, match_type = cross_id_match, "librenms_id"
+                found_as_cross_model = True
 
-        new_device, match_type = _lookup_in_model(Model)
+        if not new_device:
+            new_device, match_type = _lookup_by_name_in_model(Model)
 
         if not new_device:
             # Try the opposite model: catches cross-model imports that happened
             # after the cache was built (e.g. LibreNMS device imported as VM).
-            new_device, match_type = _lookup_in_model(CrossModel)
+            new_device, match_type = _lookup_by_name_in_model(CrossModel)
             if new_device:
                 found_as_cross_model = True
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@netbox_librenms_plugin/import_utils/bulk_import.py` around lines 590 - 637,
The issue is that _lookup_in_model(Model) tries name-based matches
(resolved_name, hostname, sysname) before considering CrossModel, so if a
librenms_id is already linked to a VM but a Device shares the same hostname, the
Device will be matched by name instead of the ID-linked VM. Reorganize the
lookup logic to prioritize librenms_id matches across both Model and CrossModel
before attempting any name-based fallbacks. Check if librenms_id exists in Model
first, then CrossModel, and only if neither has an ID match should you proceed
to the name-based matching in _lookup_in_model.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@netbox_librenms_plugin/import_utils/device_operations.py`:
- Around line 740-853: The issue is that when duplicate hostname or serial peers
are detected in the merge-candidates detection block (in the conditions checking
len() > 1), the code only appends a warning message to result["warnings"] but
leaves the stale existing_device match (originally set via .first()) still
populated in the result. This means subsequent logic can act on an arbitrary
device. To fix this, when the duplicate peer check finds multiple devices (len >
1 for either hostname or serial), clear or block the corresponding
result["existing_device"] and result["existing_match_type"] values so the stale
match doesn't propagate to later operations. This should happen in the same code
paths where you currently append the duplicate-device warnings (around the
multiple-peer detection conditions), ensuring the uniqueness validation happens
before the result is used by apply_merge_candidates or other downstream logic.

In `@netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.js`:
- Around line 1309-1313: The Escape key handler currently suppresses the outer
modal close when a nested modal is detected (via the eventStartedInNestedModal
check), but it doesn't close the nested modal itself when Bootstrap is
unavailable. Instead of simply returning when a nested modal is open, add a
fallback that manually closes the nested modal by removing the show class and
associated backdrop from the nested dialog element when Bootstrap is not
available. Follow the pattern of trying Bootstrap.Modal first and then falling
back to manual DOM manipulation to hide the nested modal.

In `@netbox_librenms_plugin/tables/device_status.py`:
- Around line 520-544: The guard condition evaluating whether to render the Host
state (starting at line 523) does not validate that the coerced paired_oob_id is
actually valid. When _coerce_pair_id(paired_oob_id) returns None for a malformed
ID, the condition still passes because None differs from a valid host ID,
causing bogus Host titles to render. Tighten the guard by adding an additional
check to ensure _coerce_pair_id(paired_oob_id) is not None (returning a valid
ID) before allowing the paired host styling to be applied, so that malformed
paired_oob_id values fall through to the generic details state instead.

In
`@netbox_librenms_plugin/templates/netbox_librenms_plugin/_interface_sync_content.html`:
- Around line 333-354: The checkboxes in this template lack accessible labels
for screen readers. Add an aria-label attribute to the select-all checkbox with
id "select-all-netbox-interfaces" to describe its function (selecting all NetBox
interfaces), and add an aria-label to each per-row checkbox with name
"interface_ids" to identify which interface row it corresponds to (you can
reference the interface.name or similar property in the label text).
- Around line 367-384: The confirmation dialog for the Move button in migrated
mode currently contains misleading language about permanent deletion, but should
instead accurately describe the action as moving or reassigning the interface.
Update the hx-confirm attribute on the button with class btn-outline-warning
within the migrated_to_winner conditional block to ensure the confirmation
message clearly indicates that the interface (and its associated
cables/IPs/MACs) is being moved/reassigned to the winner interface, not deleted.
Verify that the warning text matches the actual behavior of the
interface_move_to_winner action.

In `@netbox_librenms_plugin/tests/test_badge_contrast.py`:
- Around line 32-40: The regex pattern `_CLASS_ATTR` currently only matches
class attributes with double quotes on a single line, which causes false
negatives when class attributes use single quotes. Update the `_CLASS_ATTR`
regex pattern to match both single-quoted and double-quoted class attributes by
modifying the pattern to accept either quote type around the attribute value.
This ensures the _bare_badge_offenders function correctly identifies all
badge-related class attributes regardless of which quote style is used.

In `@netbox_librenms_plugin/tests/test_coverage_sync_views2.py`:
- Around line 1644-1653: The test is patching the `reverse()` function from
`netbox_librenms_plugin.views.sync.vlans`, which prevents it from validating
whether the actual URL name used in the VLAN sync redirect is correct. Remove
the patch for `reverse()` from the context manager in this test method and the
other similar success-path tests (the ones on lines around 1681-1690 and
1710-1719) so that the real `reverse()` function executes and validates that the
URL name passed to it matches a valid plugin route. This ensures the test fails
if the view attempts to redirect to an invalid URL name.

In `@netbox_librenms_plugin/tests/test_import_utils.py`:
- Around line 1294-1297: The mock setup in the duplicate-detection tests uses a
generic MagicMock chain that returns existing for any
Device.objects.filter().first() call regardless of the filter criteria applied.
This masks whether the implementation queries by the resolved name (core-switch)
or the raw hostname (10.0.0.1), so the test no longer verifies its documented
behavior. Modify the mock setup to be path-sensitive by configuring different
return values based on the specific filter arguments passed to
Device.objects.filter(), ensuring that only queries filtering by the resolved
name return existing while other filter queries behave differently. This
approach will make the test fail if the implementation queries the wrong
hostname variant.

---

Outside diff comments:
In `@netbox_librenms_plugin/import_utils/bulk_import.py`:
- Around line 590-637: The issue is that _lookup_in_model(Model) tries
name-based matches (resolved_name, hostname, sysname) before considering
CrossModel, so if a librenms_id is already linked to a VM but a Device shares
the same hostname, the Device will be matched by name instead of the ID-linked
VM. Reorganize the lookup logic to prioritize librenms_id matches across both
Model and CrossModel before attempting any name-based fallbacks. Check if
librenms_id exists in Model first, then CrossModel, and only if neither has an
ID match should you proceed to the name-based matching in _lookup_in_model.

In `@netbox_librenms_plugin/tests/test_coverage_sync_views2.py`:
- Around line 1257-1282: The test currently uses interface_name "eth0" which
could match by name fallback even if the port_id lookup is ignored, making the
test unable to distinguish between the librenms_id path and name fallback path.
Change the interface_name value in the ip_data dictionary to a deliberately
stale or non-matching name (different from "eth0") while keeping port_id=9
unchanged. This ensures the test will only pass if the _match_interface()
function correctly uses the port_id lookup and does not regress to relying
solely on name matching.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7203dc58-5623-46cb-b5d9-53341b2da513

📥 Commits

Reviewing files that changed from the base of the PR and between e6def35 and aece92b.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (89)
  • .gitignore
  • docs/SUMMARY.md
  • docs/feature_list.md
  • docs/librenms_import/validation.md
  • docs/usage_tips/custom_field.md
  • docs/usage_tips/oob_management.md
  • mkdocs.yml
  • netbox_librenms_plugin/constants.py
  • netbox_librenms_plugin/import_utils/__init__.py
  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/import_utils/collisions.py
  • netbox_librenms_plugin/import_utils/device_operations.py
  • netbox_librenms_plugin/import_validation_helpers.py
  • netbox_librenms_plugin/librenms_api.py
  • netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.js
  • netbox_librenms_plugin/tables/cables.py
  • netbox_librenms_plugin/tables/device_status.py
  • netbox_librenms_plugin/tables/interfaces.py
  • netbox_librenms_plugin/tables/ipaddresses.py
  • netbox_librenms_plugin/tables/modules.py
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/_cable_sync_content.html
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/_interface_sync.html
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/_interface_sync_content.html
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/_ipaddress_sync_content.html
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/_module_sync_content.html
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/_vlan_sync_content.html
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/_dt_mapping_form.html
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/_oob_interface_select.html
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/_platform_mapping_form.html
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/bulk_import_collision.html
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/create_platform_modal.html
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_import_row.html
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/librenms_sync_base.html
  • netbox_librenms_plugin/tests/conftest.py
  • netbox_librenms_plugin/tests/test_badge_contrast.py
  • netbox_librenms_plugin/tests/test_cable_sync_content_template.py
  • netbox_librenms_plugin/tests/test_cable_verify.py
  • netbox_librenms_plugin/tests/test_collisions.py
  • netbox_librenms_plugin/tests/test_coverage_actions.py
  • netbox_librenms_plugin/tests/test_coverage_base_views.py
  • netbox_librenms_plugin/tests/test_coverage_base_views2.py
  • netbox_librenms_plugin/tests/test_coverage_bulk_import.py
  • netbox_librenms_plugin/tests/test_coverage_device_fields.py
  • netbox_librenms_plugin/tests/test_coverage_device_operations.py
  • netbox_librenms_plugin/tests/test_coverage_devices.py
  • netbox_librenms_plugin/tests/test_coverage_mixins.py
  • netbox_librenms_plugin/tests/test_coverage_sync_interfaces.py
  • netbox_librenms_plugin/tests/test_coverage_sync_view.py
  • netbox_librenms_plugin/tests/test_coverage_sync_views.py
  • netbox_librenms_plugin/tests/test_coverage_sync_views2.py
  • netbox_librenms_plugin/tests/test_coverage_tables.py
  • netbox_librenms_plugin/tests/test_coverage_utils.py
  • netbox_librenms_plugin/tests/test_device_validation_details_template.py
  • netbox_librenms_plugin/tests/test_import_utils.py
  • netbox_librenms_plugin/tests/test_import_validation_helpers.py
  • netbox_librenms_plugin/tests/test_interface_sync_content_template.py
  • netbox_librenms_plugin/tests/test_ip_verify.py
  • netbox_librenms_plugin/tests/test_librenms_api.py
  • netbox_librenms_plugin/tests/test_librenms_id.py
  • netbox_librenms_plugin/tests/test_migrate_views.py
  • netbox_librenms_plugin/tests/test_module_sync_content_template.py
  • netbox_librenms_plugin/tests/test_modules_view.py
  • netbox_librenms_plugin/tests/test_permissions.py
  • netbox_librenms_plugin/tests/test_reviewer_fixes.py
  • netbox_librenms_plugin/tests/test_server_key_in_redirects.py
  • netbox_librenms_plugin/tests/test_sync_devices.py
  • netbox_librenms_plugin/tests/test_sync_modules.py
  • netbox_librenms_plugin/tests/test_sync_view_mismatch.py
  • netbox_librenms_plugin/tests/test_tables_modules.py
  • netbox_librenms_plugin/tests/test_utils.py
  • netbox_librenms_plugin/tests/test_vlan_sync.py
  • netbox_librenms_plugin/urls.py
  • netbox_librenms_plugin/utils.py
  • netbox_librenms_plugin/views/__init__.py
  • netbox_librenms_plugin/views/base/cables_view.py
  • netbox_librenms_plugin/views/base/interfaces_view.py
  • netbox_librenms_plugin/views/base/ip_addresses_view.py
  • netbox_librenms_plugin/views/base/librenms_sync_view.py
  • netbox_librenms_plugin/views/base/modules_view.py
  • netbox_librenms_plugin/views/base/vlan_table_view.py
  • netbox_librenms_plugin/views/imports/actions.py
  • netbox_librenms_plugin/views/mixins.py
  • netbox_librenms_plugin/views/object_sync/devices.py
  • netbox_librenms_plugin/views/sync/device_fields.py
  • netbox_librenms_plugin/views/sync/interfaces.py
  • netbox_librenms_plugin/views/sync/ip_addresses.py
  • netbox_librenms_plugin/views/sync/migrate.py
  • netbox_librenms_plugin/views/sync/modules.py

Comment thread netbox_librenms_plugin/import_utils/device_operations.py
Comment thread netbox_librenms_plugin/tables/device_status.py Outdated
Comment thread netbox_librenms_plugin/tests/test_badge_contrast.py Outdated
Comment thread netbox_librenms_plugin/tests/test_coverage_sync_views2.py
Comment thread netbox_librenms_plugin/tests/test_import_utils.py Outdated
@marcinpsk
marcinpsk force-pushed the feat/bulk-import branch 2 times, most recently from b353b95 to bcf9cf1 Compare June 20, 2026 05:35
@marcinpsk

Copy link
Copy Markdown
Owner Author

@coderabbitai Both outside-diff findings are develop-inherited, so per our stacked-PR convention they’re handled on the develop-targeted branch (fix/develop-hardening, PR bonzo81#311/#116), not in this feature PR:

  • tests/test_coverage_sync_views2.py:1257-1282 (make the VM interface test fail if the port-id lookup regresses) → declined here: the equivalent test on fix/develop-hardening (test_vm_interface_resolved) resolves via interface_url, not a port_id/name pair, so it doesn’t have the name-fallback masking this finding describes.
  • import_utils/bulk_import.py:590-637 (prefer any librenms_id hit before name fallbacks across models) → fixed on fix/develop-hardening in d19b555: an exact librenms_id match now wins across both models before any name fallback; real test (id-linked VM vs name-colliding Device) confirmed red against the unfixed code.

@coderabbitai

coderabbitai Bot commented Jun 20, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@marcinpsk Thanks for the clear explanation on both.

  • test_coverage_sync_views2.py:1257-1282 — Understood. Since the develop-branch test resolves via interface_url (not a port_id/name fallback path), the masking concern I raised doesn't apply there. No action needed here.
  • import_utils/bulk_import.py:590-637 — Confirmed; the fix in d19b555 on fix/develop-hardening (exact librenms_id match winning across both models before any name fallback, with a red-test guard) is the right place for this. Glad to hear the regression test was validated against the unfixed code.

The stacked-PR convention makes sense — I'll keep it in mind for future reviews of this PR series.


✏️ Learnings added
Learnt from: marcinpsk
URL: https://github.com/marcinpsk/netbox-librenms-plugin/pull/115

Timestamp: 2026-06-20T05:55:52.153Z
Learning: This repository follows a stacked-PR convention: "develop-inherited" issues (bugs or gaps that originate in the `develop` branch rather than in the feature PR's own diff) are handled on a separate develop-targeted branch (e.g., `fix/develop-hardening`) rather than in the feature PR. When reviewing feature PRs, outside-diff findings that are develop-inherited should not be requested as changes on the feature PR; the author will address them on the appropriate hardening branch.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@marcinpsk
marcinpsk force-pushed the feat/bulk-import branch 3 times, most recently from a82a5f8 to df5eeeb Compare June 20, 2026 14:05
@marcinpsk

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 20, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
netbox_librenms_plugin/views/base/interfaces_view.py (2)

261-265: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Keep internal LibreNMS IDs out of the UI warning.

The server log already includes oob["id"]; the toast should stay generic so internal LibreNMS identifiers are not exposed in user-facing UI.

🛡️ Proposed fix
                 messages.warning(
                     request,
-                    f"Interfaces refreshed, but OOB controller ports fetch failed (OOB id {oob['id']}); "
+                    "Interfaces refreshed, but OOB controller ports fetch failed; "
                     "showing host interfaces only. See server logs for details.",
                 )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@netbox_librenms_plugin/views/base/interfaces_view.py` around lines 261 - 265,
In the messages.warning call that displays the OOB controller ports fetch
failure message, remove the internal LibreNMS identifier from the user-facing UI
message by deleting the interpolated oob['id'] portion from the f-string. Keep
the warning message generic for end users (such as "OOB controller ports fetch
failed") without exposing internal IDs, while relying on server logs to capture
the detailed identification information needed for debugging.

153-156: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve migrated mode when the posted server key is stale.

This branch redirects without the POSTed server_key and without build_migrated_context(). On a migrated donor whose marker is namespaced to the stale key, the HTMX refresh can come back without the migrated marker and re-enable normal interface sync controls.

Based on learnings, POST handlers in base/sync views should propagate the POST-scoped server_key for cache and redirect/tab state.

🐛 Proposed fix
-        post_server_key = self.rebind_api_for_server(request.POST.get("server_key"))
+        posted_server_key = request.POST.get("server_key")
+        post_server_key = self.rebind_api_for_server(posted_server_key)
         if post_server_key is None:
             messages.error(request, "Selected LibreNMS server is no longer configured.")
-            return redirect(self.get_redirect_url(obj))
+            return render(
+                request,
+                self.partial_template_name,
+                {
+                    "interface_sync": {
+                        "object": obj,
+                        "table": None,
+                        "vlan_groups": [],
+                        "last_fetched": None,
+                        "cache_expiry": None,
+                        "virtual_chassis_members": [],
+                        "interface_name_field": interface_name_field,
+                        "netbox_only_interfaces": [],
+                        "server_key": None,
+                        "oob_incomplete": False,
+                    },
+                    "interface_name_field": interface_name_field,
+                    **build_migrated_context(obj, posted_server_key),
+                },
+            )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@netbox_librenms_plugin/views/base/interfaces_view.py` around lines 153 - 156,
When post_server_key is None (indicating a stale server key), the redirect to
get_redirect_url(obj) does not preserve the migrated context. Pass the original
stale server_key from request.POST.get("server_key") as a parameter to
get_redirect_url() to maintain the migrated mode and ensure the HTMX refresh
preserves the migrated marker that is namespaced to that stale key. This ensures
the tab state and cache remain consistent for the migrated donor context.

Source: Learnings

netbox_librenms_plugin/views/base/cables_view.py (1)

225-245: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fail OOB cable refresh when OOB port metadata is unavailable.

If get_ports(oob["id"]) fails or returns malformed data, the maps stay empty but OOB link rows are still appended and cached as a successful snapshot. For LibreNMS link rows that only carry local_port_id, this drops local_port resolution and can overwrite a good cache with unverifiable OOB cable rows.

🐛 Proposed fix
                 oob_ports_success, oob_ports_data = self.librenms_api.get_ports(oob["id"])
                 oob_local_ports_map = {}
                 oob_local_ports_alt_map = {}
-                if oob_ports_success and isinstance(oob_ports_data, dict):
-                    oob_ports = oob_ports_data.get("ports")
-                    for port in oob_ports if isinstance(oob_ports, list) else []:
+                oob_ports_ok = (
+                    oob_ports_success
+                    and isinstance(oob_ports_data, dict)
+                    and isinstance(oob_ports_data.get("ports"), list)
+                    and all(isinstance(port, dict) for port in oob_ports_data["ports"])
+                )
+                if not oob_ports_ok:
+                    self._oob_links_fetch_failed = True
+                    logger.warning(
+                        "OOB ports fetch failed for device %s (OOB id %s): %s",
+                        self.librenms_id,
+                        oob["id"],
+                        oob_ports_data,
+                    )
+                else:
+                    for port in oob_ports_data["ports"]:
                         # Skip non-dict rows (see the main-branch guard above) so a
                         # malformed OOB ports payload can't 500 the refresh.
-                        if not isinstance(port, dict):
-                            continue
                         raw_port_id = port.get("port_id")
                         if raw_port_id is None:
                             continue
                         port_name = port.get(interface_name_field)
                         if port_name is None:
                             continue
                         oob_local_ports_map[str(raw_port_id)] = port_name
                         # Same alternate-field fallback as the main branch (issue `#88`).
                         alt_name = port.get(alt_name_field)
                         if alt_name and alt_name != port_name:
                             oob_local_ports_alt_map[str(raw_port_id)] = alt_name
+
+                    # Only merge OOB links after the port-id -> name map is trustworthy.
+                    oob_links = oob_data.get("links")
+                    ...

Also applies to: 263-283

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@netbox_librenms_plugin/views/base/cables_view.py` around lines 225 - 245, The
issue is that when the get_ports API call fails (oob_ports_success is False) or
returns malformed data (oob_ports_data is not a dict), the oob_local_ports_map
and oob_local_ports_alt_map dictionaries remain empty, but the OOB cable rows
are still appended and cached as a successful snapshot. To fix this, add a guard
check after the if block that populates these maps: if oob_ports_success is
False or oob_ports_data is not a dict, use continue to skip processing this OOB
device entirely and move to the next OOB iteration. This prevents unverifiable
OOB cable rows with empty port metadata from overwriting the cache. Apply the
same fix to the similar code mentioned in the also applies to section at lines
263-283.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@netbox_librenms_plugin/views/base/cables_view.py`:
- Around line 225-245: The issue is that when the get_ports API call fails
(oob_ports_success is False) or returns malformed data (oob_ports_data is not a
dict), the oob_local_ports_map and oob_local_ports_alt_map dictionaries remain
empty, but the OOB cable rows are still appended and cached as a successful
snapshot. To fix this, add a guard check after the if block that populates these
maps: if oob_ports_success is False or oob_ports_data is not a dict, use
continue to skip processing this OOB device entirely and move to the next OOB
iteration. This prevents unverifiable OOB cable rows with empty port metadata
from overwriting the cache. Apply the same fix to the similar code mentioned in
the also applies to section at lines 263-283.

In `@netbox_librenms_plugin/views/base/interfaces_view.py`:
- Around line 261-265: In the messages.warning call that displays the OOB
controller ports fetch failure message, remove the internal LibreNMS identifier
from the user-facing UI message by deleting the interpolated oob['id'] portion
from the f-string. Keep the warning message generic for end users (such as "OOB
controller ports fetch failed") without exposing internal IDs, while relying on
server logs to capture the detailed identification information needed for
debugging.
- Around line 153-156: When post_server_key is None (indicating a stale server
key), the redirect to get_redirect_url(obj) does not preserve the migrated
context. Pass the original stale server_key from request.POST.get("server_key")
as a parameter to get_redirect_url() to maintain the migrated mode and ensure
the HTMX refresh preserves the migrated marker that is namespaced to that stale
key. This ensures the tab state and cache remain consistent for the migrated
donor context.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro

Run ID: a4886672-b25c-41b5-9595-963ba0f7d71a

📥 Commits

Reviewing files that changed from the base of the PR and between aece92b and df5eeeb.

📒 Files selected for processing (62)
  • netbox_librenms_plugin/constants.py
  • netbox_librenms_plugin/import_utils/__init__.py
  • netbox_librenms_plugin/import_utils/bulk_import.py
  • netbox_librenms_plugin/import_utils/collisions.py
  • netbox_librenms_plugin/import_utils/device_operations.py
  • netbox_librenms_plugin/librenms_api.py
  • netbox_librenms_plugin/static/netbox_librenms_plugin/js/librenms_import.js
  • netbox_librenms_plugin/tables/device_status.py
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/_interface_sync_content.html
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/_ipaddress_sync_content.html
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/bulk_import_collision.html
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
  • netbox_librenms_plugin/tests/conftest.py
  • netbox_librenms_plugin/tests/test_badge_contrast.py
  • netbox_librenms_plugin/tests/test_cable_verify.py
  • netbox_librenms_plugin/tests/test_collisions.py
  • netbox_librenms_plugin/tests/test_coverage_actions.py
  • netbox_librenms_plugin/tests/test_coverage_base_views.py
  • netbox_librenms_plugin/tests/test_coverage_base_views2.py
  • netbox_librenms_plugin/tests/test_coverage_bulk_import.py
  • netbox_librenms_plugin/tests/test_coverage_device_fields.py
  • netbox_librenms_plugin/tests/test_coverage_device_operations.py
  • netbox_librenms_plugin/tests/test_coverage_devices.py
  • netbox_librenms_plugin/tests/test_coverage_mixins.py
  • netbox_librenms_plugin/tests/test_coverage_sync_interfaces.py
  • netbox_librenms_plugin/tests/test_coverage_sync_view.py
  • netbox_librenms_plugin/tests/test_coverage_sync_views.py
  • netbox_librenms_plugin/tests/test_coverage_sync_views2.py
  • netbox_librenms_plugin/tests/test_coverage_tables.py
  • netbox_librenms_plugin/tests/test_coverage_utils.py
  • netbox_librenms_plugin/tests/test_device_validation_details_template.py
  • netbox_librenms_plugin/tests/test_import_utils.py
  • netbox_librenms_plugin/tests/test_import_validation_helpers.py
  • netbox_librenms_plugin/tests/test_interface_sync_content_template.py
  • netbox_librenms_plugin/tests/test_ip_verify.py
  • netbox_librenms_plugin/tests/test_ipaddress_sync_content_template.py
  • netbox_librenms_plugin/tests/test_librenms_api.py
  • netbox_librenms_plugin/tests/test_librenms_id.py
  • netbox_librenms_plugin/tests/test_migrate_views.py
  • netbox_librenms_plugin/tests/test_modules_view.py
  • netbox_librenms_plugin/tests/test_permissions.py
  • netbox_librenms_plugin/tests/test_reviewer_fixes.py
  • netbox_librenms_plugin/tests/test_server_key_in_redirects.py
  • netbox_librenms_plugin/tests/test_sync_modules.py
  • netbox_librenms_plugin/tests/test_tables_modules.py
  • netbox_librenms_plugin/tests/test_utils.py
  • netbox_librenms_plugin/tests/test_vlan_sync.py
  • netbox_librenms_plugin/urls.py
  • netbox_librenms_plugin/utils.py
  • netbox_librenms_plugin/views/__init__.py
  • netbox_librenms_plugin/views/base/cables_view.py
  • netbox_librenms_plugin/views/base/interfaces_view.py
  • netbox_librenms_plugin/views/base/ip_addresses_view.py
  • netbox_librenms_plugin/views/base/librenms_sync_view.py
  • netbox_librenms_plugin/views/base/modules_view.py
  • netbox_librenms_plugin/views/base/vlan_table_view.py
  • netbox_librenms_plugin/views/imports/actions.py
  • netbox_librenms_plugin/views/mixins.py
  • netbox_librenms_plugin/views/sync/device_fields.py
  • netbox_librenms_plugin/views/sync/ip_addresses.py
  • netbox_librenms_plugin/views/sync/migrate.py
  • netbox_librenms_plugin/views/sync/modules.py

@marcinpsk

Copy link
Copy Markdown
Owner Author

@coderabbitai — the three outside-diff (body-only) findings from the latest review on this PR:

  • views/base/interfaces_view.py:261-265 — “Keep internal LibreNMS IDs out of the UI warning.” Stale location: those lines now hold the main/OOB MAC-dedup logic, not a UI warning. The earlier id-in-warning exposure was already addressed (the ambiguous-librenms_id UI text was dropped in 5dc141ed). Reads as a stale outside-diff recurrence — if you can point at a current line that still renders an internal LibreNMS id in a user-facing warning, I’ll fix it.

  • views/base/interfaces_view.py:153-156 — “Preserve migrated mode when the posted server key is stale.” Migrated mode is preserved: when the POSTed server_key no longer resolves, the interfaces refresh messages.error(...) and redirects to the sync page, whose GET rebuilds migrated context via build_migrated_context() in get_context_data — so the donor lands back in migrated mode. (The IP view does this inline rather than via redirect; both end up in migrated mode.) The cited line is also stale (now duplicate-librenms_id de-duplication).

  • views/base/cables_view.py:225-245 — “Fail OOB cable refresh when OOB port metadata is unavailable.” Declined. The raw-local_port fallback on a failed OOB ports fetch is intentional and consistent with the host-device branch, which does exactly the same (cables_view.py ~191-194: “Fall back to the LibreNMS-reported local_port name … when the ports map misses or ports fetch failed”). It’s explicitly covered by test_get_links_data_oob_falls_back_to_raw_name_on_port_fetch_failure. The OOB links fetch-failure and malformed-payload paths already fail closed (_oob_links_fetch_failed); failing closed additionally on a ports-metadata miss would drop valid OOB cable rows (which still carry usable LLDP-reported names) and make OOB inconsistent with the host path.

@coderabbitai

coderabbitai Bot commented Jun 20, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@marcinpsk Thanks for the detailed walkthrough on all three.

  1. interfaces_view.py:261-265 — Withdrawn. You're right that the cited location is stale — if the ambiguous-librenms_id UI text was already removed in 5dc141ed, the finding no longer applies. Nothing to action here.

  2. interfaces_view.py:153-156 — Withdrawn. The redirect-on-stale-key → GET-rebuilds-migrated-context flow is a valid and complete path for preserving migrated mode. The cited line being stale confirms this was an outside-diff misfire.

  3. cables_view.py:225-245 — Acknowledged, declining recorded. The symmetry argument is sound: falling back to the raw LLDP-reported local_port name on a ports-metadata miss is the same policy as the host branch, keeps OOB cable rows surfaced, and the distinction between links-fetch failure (fail-closed via _oob_links_fetch_failed) vs ports-metadata miss (graceful fallback) is a deliberate design boundary — not an inconsistency. No change needed.


✏️ Learnings added
Learnt from: marcinpsk
URL: https://github.com/marcinpsk/netbox-librenms-plugin/pull/115

Timestamp: 2026-06-20T22:53:48.855Z
Learning: In `netbox_librenms_plugin/views/base/cables_view.py`, falling back to the raw LibreNMS-reported `local_port` name when OOB port metadata is unavailable (ports fetch failed or ports map miss) is intentional and mirrors the host-device branch behavior (~lines 191-194). The OOB *links* fetch-failure and malformed-payload paths do fail closed via `_oob_links_fetch_failed`; the ports-metadata miss path uses a graceful name fallback to avoid dropping valid OOB cable rows that still carry usable LLDP-reported names. This is covered by `test_get_links_data_oob_falls_back_to_raw_name_on_port_fetch_failure`.

Learnt from: marcinpsk
URL: https://github.com/marcinpsk/netbox-librenms-plugin/pull/115

Timestamp: 2026-06-20T22:53:48.855Z
Learning: In `netbox_librenms_plugin/views/base/interfaces_view.py`, when the POSTed `server_key` is stale/no longer resolves during an interface refresh, the view emits `messages.error(...)` and redirects to the sync page. The sync page's GET handler then rebuilds migrated context via `build_migrated_context()` in `get_context_data()`, so the donor device correctly lands back in migrated mode. The IP view handles the same scenario inline (rather than via redirect), but both paths end up in migrated mode.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@marcinpsk

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 21, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
netbox_librenms_plugin/views/base/vlan_table_view.py (1)

48-57: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Keep the migrated-context fallback when the POST key is empty.

If server_key is missing or blank here, build_migrated_context() gets an empty value and the retry fragment can render without the donor/winner controls.

Proposed fix
-                **build_migrated_context(obj, request.POST.get("server_key")),
+                **build_migrated_context(
+                    obj,
+                    request.POST.get("server_key") or self.librenms_api.server_key,
+                ),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@netbox_librenms_plugin/views/base/vlan_table_view.py` around lines 48 - 57,
The issue is that when the POST key "server_key" is missing or empty, the call
to build_migrated_context(obj, request.POST.get("server_key")) receives a None
or empty value, which prevents the donor/winner controls from being properly
preserved in the error context. Fix this by providing a fallback value when
request.POST.get("server_key") returns None or is empty. The fallback should
extract the server_key from the object itself or another appropriate source to
ensure the migrated-context flags are properly maintained when rendering the
retry fragment.
netbox_librenms_plugin/views/base/cables_view.py (1)

611-625: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fall back to the active server key when preserving migrated context.

Line 625 passes posted_server_key directly to build_migrated_context(). When the POST omits or sends a blank server_key, this becomes None/blank and can miss the donor’s server-scoped _migrated_to marker, re-enabling the normal cable-sync controls in the stale-server error partial.

Proposed fix
-        posted_server_key = request.POST.get("server_key")
+        posted_server_key = (request.POST.get("server_key") or "").strip()
+        migrated_server_key = (
+            posted_server_key
+            or getattr(getattr(self, "_librenms_api", None), "server_key", None)
+            or "default"
+        )
         # Rebind the API to the POSTed server so live link/port fetches hit the same
         # LibreNMS instance the cached rows are namespaced under (multi-server tabs).
         server_key = self.rebind_api_for_server(posted_server_key)
@@
                     "cable_sync": {"object": obj, "table": None, "cache_expiry": None, "server_key": None},
-                    **build_migrated_context(obj, posted_server_key),
+                    **build_migrated_context(obj, migrated_server_key),
                 },
             )

Based on learnings, POST handlers in views/base should keep server-scoped state using the POSTed server_key with a fallback to the active self.librenms_api.server_key.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@netbox_librenms_plugin/views/base/cables_view.py` around lines 611 - 625, The
issue is that on line 625, posted_server_key is passed directly to
build_migrated_context() without a fallback, so when the POST omits or sends a
blank server_key value (making it None or blank), the donor's server-scoped
_migrated_to marker is missed. Fix this by modifying the call to
build_migrated_context(obj, posted_server_key) to use a fallback pattern: pass
posted_server_key if it is valid and non-empty, otherwise fall back to
self.librenms_api.server_key to preserve the server-scoped migrated context
correctly.

Source: Learnings

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@netbox_librenms_plugin/tests/test_migrate_views.py`:
- Around line 881-890: Replace the hardcoded GATE template string in the
TestMigratedTransferIpDeviceOnlyGate class with the actual production template
or a reference to the real librenms_sync_base.html file. Update the _render
method to load and render the actual production template section containing the
transfer-IP controls instead of rendering the copied mini-template defined in
GATE. This ensures the regression test verifies the actual shipped template
source and will catch if someone removes or changes the device-only guard from
the real template.

In `@netbox_librenms_plugin/views/imports/actions.py`:
- Around line 2849-2886: The current code locks existing_device first with
select_for_update(), then later calls find_by_librenms_id with
select_for_update=True to check for conflicts, which can cause a deadlock when
two concurrent promotions occur in opposite directions (each holding one device
lock and waiting for the other). Refactor to acquire both relevant device row
locks in a deterministic order (such as by primary key) first before performing
any validation logic, similar to how the merge flow handles this. This ensures
locks are always acquired in the same order across all concurrent operations,
preventing the deadlock scenario.

---

Outside diff comments:
In `@netbox_librenms_plugin/views/base/cables_view.py`:
- Around line 611-625: The issue is that on line 625, posted_server_key is
passed directly to build_migrated_context() without a fallback, so when the POST
omits or sends a blank server_key value (making it None or blank), the donor's
server-scoped _migrated_to marker is missed. Fix this by modifying the call to
build_migrated_context(obj, posted_server_key) to use a fallback pattern: pass
posted_server_key if it is valid and non-empty, otherwise fall back to
self.librenms_api.server_key to preserve the server-scoped migrated context
correctly.

In `@netbox_librenms_plugin/views/base/vlan_table_view.py`:
- Around line 48-57: The issue is that when the POST key "server_key" is missing
or empty, the call to build_migrated_context(obj,
request.POST.get("server_key")) receives a None or empty value, which prevents
the donor/winner controls from being properly preserved in the error context.
Fix this by providing a fallback value when request.POST.get("server_key")
returns None or is empty. The fallback should extract the server_key from the
object itself or another appropriate source to ensure the migrated-context flags
are properly maintained when rendering the retry fragment.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4a799f44-9fab-4a6b-bb50-bacaac8d5e33

📥 Commits

Reviewing files that changed from the base of the PR and between df5eeeb and 6ce26d8.

📒 Files selected for processing (27)
  • netbox_librenms_plugin/import_utils/__init__.py
  • netbox_librenms_plugin/import_utils/collisions.py
  • netbox_librenms_plugin/import_utils/device_operations.py
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/bulk_import_collision.html
  • netbox_librenms_plugin/templates/netbox_librenms_plugin/htmx/device_validation_details.html
  • netbox_librenms_plugin/tests/test_collisions.py
  • netbox_librenms_plugin/tests/test_coverage_actions.py
  • netbox_librenms_plugin/tests/test_coverage_base_views.py
  • netbox_librenms_plugin/tests/test_coverage_base_views2.py
  • netbox_librenms_plugin/tests/test_coverage_device_operations.py
  • netbox_librenms_plugin/tests/test_device_validation_details_template.py
  • netbox_librenms_plugin/tests/test_interface_sync_content_template.py
  • netbox_librenms_plugin/tests/test_librenms_id.py
  • netbox_librenms_plugin/tests/test_migrate_views.py
  • netbox_librenms_plugin/tests/test_modules_view.py
  • netbox_librenms_plugin/tests/test_vlan_sync.py
  • netbox_librenms_plugin/urls.py
  • netbox_librenms_plugin/utils.py
  • netbox_librenms_plugin/views/__init__.py
  • netbox_librenms_plugin/views/base/cables_view.py
  • netbox_librenms_plugin/views/base/interfaces_view.py
  • netbox_librenms_plugin/views/base/ip_addresses_view.py
  • netbox_librenms_plugin/views/base/librenms_sync_view.py
  • netbox_librenms_plugin/views/base/modules_view.py
  • netbox_librenms_plugin/views/base/vlan_table_view.py
  • netbox_librenms_plugin/views/imports/actions.py
  • netbox_librenms_plugin/views/sync/migrate.py

Comment thread netbox_librenms_plugin/tests/test_migrate_views.py Outdated
Comment thread netbox_librenms_plugin/views/imports/actions.py Outdated
@marcinpsk

Copy link
Copy Markdown
Owner Author

@coderabbitai — the outside-diff (body-only) findings from the latest review:

  • views/base/vlan_table_view.py:48-57 — keep the migrated-context fallback when the POST key is empty/stale. Fixed in ce974775: the VLAN stale-server path now resolves build_migrated_context(obj, self.librenms_api.server_key) — the session/active key, never the failed-to-rebind POSTed key (which would miss the marker and re-enable a migrated donor's controls). Real-DB test added.
  • views/base/cables_view.py:611-625 — fall back to the active server key when preserving migrated context. Fixed in d6f3f41c: same one-line fix on the cables stale-server path; tests extended to a non-empty stale key (red→green).

@coderabbitai

coderabbitai Bot commented Jun 21, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@marcinpsk Thanks for the follow-through on both.

  1. vlan_table_view.py:48-57 — The ce974775 fix looks correct: using self.librenms_api.server_key (the session/active key) rather than the failed-to-rebind POSTed key ensures build_migrated_context can still locate the _migrated_to marker and keeps the donor's controls suppressed. Good to have the real-DB test covering the regression path.

  2. cables_view.py:611-625 — Same one-liner fix in d6f3f41c applied symmetrically to the cables stale-server path, with the non-empty stale key red→green test confirming it.

Both are clean fixes to a subtle but important invariant (stale POST key must never be forwarded into migrated-context resolution).


✏️ Learnings added
Learnt from: marcinpsk
URL: https://github.com/marcinpsk/netbox-librenms-plugin/pull/115

Timestamp: 2026-06-21T06:04:13.816Z
Learning: In `netbox_librenms_plugin/views/base/vlan_table_view.py` and `netbox_librenms_plugin/views/base/cables_view.py`, when a POSTed `server_key` fails to rebind (stale/unknown), the fallback call to `build_migrated_context(obj, ...)` must use `self.librenms_api.server_key` (the session/active server key), never the failed POSTed key. Using the failed POSTed key would miss the `_migrated_to` marker stored under the active server key, causing a migrated donor's controls to be incorrectly re-enabled.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

@marcinpsk

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 21, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

cached_row_matches() compared coerced ids with ==, so two un-coercible
ids matched as None == None and an invalid requested id was served any
trusted cached row. Validate the requested id first: un-coercible never
matches.

The single-row live-fetch fallback in bulk_import_devices_shared()
accepted any successful payload and wrote it into the shared cache
without checking its device_id against the requested id. Extract the
collision pre-check's strict rule into row_identity_matches() and apply
it at both sites: a payload that isn't a dict carrying the requested
device_id is treated as a failed retrieval and never cached.
NetBox treats MAX_PAGE_SIZE 0/None as 'no ceiling'. min(per_page, 0)
silently returned a zero page size and min(per_page, None) raised
TypeError; pass the requested per_page through unclamped when the
ceiling is disabled.
The IP sync writes the IPAddress row and then saves the device (primary_ip4/6),
while the migrate move views lock Device first and only then the IPAddress and
its interface. Two concurrent transactions taking the two rows in opposite order
close a deadlock cycle, which Postgres resolves by aborting one side.

Take the device row lock before the address write on the primary-IP candidate
row, so both paths order their locks Device -> IPAddress, and re-read the
primary_ip ids from the locked row (a stale in-memory value would make
_set_primary_ip skip a set it must perform). Only the management-address row
pays for the extra lock; every other row is unchanged.

The same-host test is now computed once and reused for the three places that
asked for it.
@marcinpsk

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@netbox_librenms_plugin/tests/test_validation_template_server_key.py`:
- Around line 45-46: Update the validation helpers around
_form_has_named_control so server_key validation only matches an input element
with type="hidden" and name="server_key", rather than any named control such as
a button. Preserve button matching for action validation, and use the
server-key-specific matcher at the checks on lines 73 and 83.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f4d713e4-adac-4072-84b9-0b869bf62a2a

📥 Commits

Reviewing files that changed from the base of the PR and between 827132e and f929408.

📒 Files selected for processing (3)
  • netbox_librenms_plugin/tests/test_coverage_sync_interfaces.py
  • netbox_librenms_plugin/tests/test_validation_template_server_key.py
  • netbox_librenms_plugin/tests/test_vlan_sync_concurrency.py
📜 Review details
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

When building HttpResponse from Django-template-rendered HTML in views, use format_html() to compose the envelope and mark_safe() on the inner HTML to clear CodeQL py/reflected-xss false positives. Example: format_html('<div id="target" hx-swap-oob="innerHTML">{}</div>', mark_safe(modal_html))

Files:

  • netbox_librenms_plugin/tests/test_vlan_sync_concurrency.py
  • netbox_librenms_plugin/tests/test_validation_template_server_key.py
  • netbox_librenms_plugin/tests/test_coverage_sync_interfaces.py
🧠 Learnings (17)
📚 Learning: 2026-03-07T22:46:57.537Z
Learnt from: marcinpsk
Repo: marcinpsk/netbox-librenms-plugin PR: 21
File: netbox_librenms_plugin/tests/test_librenms_id.py:184-184
Timestamp: 2026-03-07T22:46:57.537Z
Learning: Do not flag or request style-only changes (such as removing redundant imports or cosmetic cleanup) in Python code reviews. Focus on issues that affect correctness, functionality, or security. This guideline applies to Python files across the repository, including tests (e.g., netbox_librenms_plugin/tests/test_librenms_id.py).

Applied to files:

  • netbox_librenms_plugin/tests/test_vlan_sync_concurrency.py
  • netbox_librenms_plugin/tests/test_validation_template_server_key.py
  • netbox_librenms_plugin/tests/test_coverage_sync_interfaces.py
📚 Learning: 2026-03-08T13:09:49.031Z
Learnt from: marcinpsk
Repo: marcinpsk/netbox-librenms-plugin PR: 22
File: netbox_librenms_plugin/forms.py:75-79
Timestamp: 2026-03-08T13:09:49.031Z
Learning: In multi-server caching within the codebase, ensure poller group choices and similar cache discriminators use api.server_key as the cache key component (e.g., cache_key = f"librenms_poller_group_choices_{api.server_key}") rather than api.librenms_url. This aligns with the fixed approach seen in commit bf37f07 and with other modules. Apply this pattern consistently to Python files under netbox_librenms_plugin (and similar multi-server cache keys) to maintain correct cross-server caching behavior.

Applied to files:

  • netbox_librenms_plugin/tests/test_vlan_sync_concurrency.py
  • netbox_librenms_plugin/tests/test_validation_template_server_key.py
  • netbox_librenms_plugin/tests/test_coverage_sync_interfaces.py
📚 Learning: 2026-05-05T09:51:15.707Z
Learnt from: marcinpsk
Repo: marcinpsk/netbox-librenms-plugin PR: 58
File: netbox_librenms_plugin/views/base/modules_view.py:311-313
Timestamp: 2026-05-05T09:51:15.707Z
Learning: In this repository, if you see `timezone.timedelta` used from `django.utils.timezone`, do not request a diff to replace it with `datetime.timedelta` solely on style grounds. This usage is functionally correct because Django exposes `timedelta` via `django.utils.timezone`; treat this as intentional and avoid churn unless there is evidence of changed/incorrect behavior (e.g., `timezone` is not Django’s module or `timedelta` is unavailable).

Applied to files:

  • netbox_librenms_plugin/tests/test_vlan_sync_concurrency.py
  • netbox_librenms_plugin/tests/test_validation_template_server_key.py
  • netbox_librenms_plugin/tests/test_coverage_sync_interfaces.py
📚 Learning: 2026-06-01T13:35:47.228Z
Learnt from: marcinpsk
Repo: marcinpsk/netbox-librenms-plugin PR: 79
File: netbox_librenms_plugin/views/sync/migrate.py:177-181
Timestamp: 2026-06-01T13:35:47.228Z
Learning: When reviewing this plugin’s permission checks, note that `check_object_permissions` / `NetBoxObjectPermissionMixin` enforce only **model-level** permissions: they call `request.user.has_perm(perm)` without any object/row instance, and the plugin does not currently implement per-object (row-level) permission scoping. Therefore, do **not** flag “missing winner-side/per-object object-permission checks” in sync/migrate views (or elsewhere in the plugin) as a defect; per-object permission scoping is an intentional plugin-wide design gap to be addressed in a dedicated future PR.

Applied to files:

  • netbox_librenms_plugin/tests/test_vlan_sync_concurrency.py
  • netbox_librenms_plugin/tests/test_validation_template_server_key.py
  • netbox_librenms_plugin/tests/test_coverage_sync_interfaces.py
📚 Learning: 2026-07-01T16:41:50.451Z
Learnt from: marcinpsk
Repo: marcinpsk/netbox-librenms-plugin PR: 113
File: netbox_librenms_plugin/tables/cables.py:62-76
Timestamp: 2026-07-01T16:41:50.451Z
Learning: When rendering Bootstrap/Tabler badges in this repo (netbox-librenms-plugin), always pair a solid background utility `bg-*` with an explicit text utility that provides appropriate contrast (e.g., `bg-danger` + `text-white`, `bg-warning` + `text-dark`, `bg-purple` + `text-white`). Follow the existing badge contrast convention enforced by `netbox_librenms_plugin/tests/test_badge_contrast.py`. Do not suggest Tabler’s semantic `text-*-fg` token classes (e.g., `text-purple-fg`), since they are not used in this codebase and are not covered by the contrast test.

Applied to files:

  • netbox_librenms_plugin/tests/test_vlan_sync_concurrency.py
  • netbox_librenms_plugin/tests/test_validation_template_server_key.py
  • netbox_librenms_plugin/tests/test_coverage_sync_interfaces.py
📚 Learning: 2026-07-30T02:40:53.531Z
Learnt from: marcinpsk
Repo: marcinpsk/netbox-librenms-plugin PR: 123
File: netbox_librenms_plugin/views/base/modules_view.py:0-0
Timestamp: 2026-07-30T02:40:53.531Z
Learning: In the NetBox LibreNMS plugin, normalize all LibreNMS serial values via `netbox_librenms_plugin.utils.normalize_serial()` before storing/comparing them. Treat only `None` as “absent”; for any other value (including numeric/falsey values like `0` or `False`), convert using `str(value).strip()` inside the normalizer. For identity/conflict checks against existing NetBox `Device` rows, compare using trimmed serial matching (i.e., compare normalized/stripped serial strings) so legacy device serials with surrounding whitespace still match normalized incoming serials.

Applied to files:

  • netbox_librenms_plugin/tests/test_vlan_sync_concurrency.py
  • netbox_librenms_plugin/tests/test_validation_template_server_key.py
  • netbox_librenms_plugin/tests/test_coverage_sync_interfaces.py
📚 Learning: 2026-08-03T19:16:41.198Z
Learnt from: marcinpsk
Repo: marcinpsk/netbox-librenms-plugin PR: 87
File: netbox_librenms_plugin/views/base/ip_addresses_view.py:502-502
Timestamp: 2026-08-03T19:16:41.198Z
Learning: For migration-marker handling in the NetBox LibreNMS plugin, use `get_migrated_to_marker()` and `mark_librenms_migrated()` as the centralized read/write chokepoints rather than duplicating marker logic. Normalize blank or `None` `server_key` values to `"default"`. Derive migration UI context with `build_migrated_context()` and pass it through `render_sync_partial()` to the interface, IP, cable, module, and VLAN sync partials.

Applied to files:

  • netbox_librenms_plugin/tests/test_vlan_sync_concurrency.py
  • netbox_librenms_plugin/tests/test_validation_template_server_key.py
  • netbox_librenms_plugin/tests/test_coverage_sync_interfaces.py
📚 Learning: 2026-03-27T02:04:22.276Z
Learnt from: marcinpsk
Repo: marcinpsk/netbox-librenms-plugin PR: 22
File: netbox_librenms_plugin/tests/test_coverage_api.py:893-939
Timestamp: 2026-03-27T02:04:22.276Z
Learning: For unit tests in this repo (e.g., coverage API tests), when testing a happy-path call like `add_device()`, assert both the success flag and the expected success message (e.g., `assert ok is True` and `assert msg == "Device added successfully."`). This ensures the test fails if `add_device()` returns `(False, ...)`. If a related assertion is explicitly tracked as a known deferred follow-up for a prior PR, do not treat the missing `ok is True` assertion as a new review finding in subsequent reviews.

Applied to files:

  • netbox_librenms_plugin/tests/test_vlan_sync_concurrency.py
  • netbox_librenms_plugin/tests/test_validation_template_server_key.py
  • netbox_librenms_plugin/tests/test_coverage_sync_interfaces.py
📚 Learning: 2026-06-02T11:11:56.131Z
Learnt from: marcinpsk
Repo: marcinpsk/netbox-librenms-plugin PR: 79
File: netbox_librenms_plugin/tests/test_coverage_actions.py:4773-4776
Timestamp: 2026-06-02T11:11:56.131Z
Learning: When application code performs a function-local import inside a method body (e.g., `from utilities.permissions import get_permission_for_model`), unit tests should patch the original source attribute (`utilities.permissions.get_permission_for_model`). Do not patch the consumer module’s name (e.g., `netbox_librenms_plugin.views.imports.actions.get_permission_for_model`) unless the function is imported at module scope and exposed as a module attribute—local imports re-resolve the attribute at call time.

Applied to files:

  • netbox_librenms_plugin/tests/test_vlan_sync_concurrency.py
  • netbox_librenms_plugin/tests/test_validation_template_server_key.py
  • netbox_librenms_plugin/tests/test_coverage_sync_interfaces.py
📚 Learning: 2026-06-02T20:43:51.604Z
Learnt from: marcinpsk
Repo: marcinpsk/netbox-librenms-plugin PR: 79
File: netbox_librenms_plugin/tests/test_coverage_device_operations.py:2466-2478
Timestamp: 2026-06-02T20:43:51.604Z
Learning: When reviewing tests under netbox_librenms_plugin/tests, don’t treat intentional stubs/mocks of lower-layer helper functions as a “coverage hole” if the test’s goal is to isolate and verify only the validate-layer (or another single unit of behavior). If the stubbed helper’s actual logic is exercised in dedicated tests at the helper/service layer (e.g., test_*_helper* / test_librenms_id.py), it’s acceptable for the validate-layer test to control helper outputs (via side_effect/return values) and assert the validate-layer mapping/selection logic only. Flag only when the stub hides untested logic that should belong to the unit under test (i.e., the test asserts behavior from the helper without actually verifying the unit’s own responsibility).

Applied to files:

  • netbox_librenms_plugin/tests/test_vlan_sync_concurrency.py
  • netbox_librenms_plugin/tests/test_validation_template_server_key.py
  • netbox_librenms_plugin/tests/test_coverage_sync_interfaces.py
📚 Learning: 2026-06-15T18:49:04.201Z
Learnt from: marcinpsk
Repo: marcinpsk/netbox-librenms-plugin PR: 104
File: netbox_librenms_plugin/tests/test_coverage_actions.py:1866-1870
Timestamp: 2026-06-15T18:49:04.201Z
Learning: When reviewing tests related to the LibreNMS device ID migration flow (e.g., `migrate_librenms_id` / `migrate_legacy_librenms_id`), do not require `validation["librenms_id_needs_migration"] == True` solely for test setup. That flag is only used for UI visibility in `device_status.py` / `device_validation_details.html`; the backend migration action is gated by the instance’s legacy raw value (`custom_field_data["librenms_id"]` matching the active LibreNMS device id) plus the `serial_confirmed` or `force` condition. If the test already pins/executes migration by asserting the migration function was called with the locked instance (or otherwise directly forces execution), it should be acceptable even when `librenms_id_needs_migration` is not set to True.

Applied to files:

  • netbox_librenms_plugin/tests/test_vlan_sync_concurrency.py
  • netbox_librenms_plugin/tests/test_validation_template_server_key.py
  • netbox_librenms_plugin/tests/test_coverage_sync_interfaces.py
📚 Learning: 2026-06-17T07:31:54.849Z
Learnt from: marcinpsk
Repo: marcinpsk/netbox-librenms-plugin PR: 104
File: netbox_librenms_plugin/tests/test_librenms_api.py:2258-2258
Timestamp: 2026-06-17T07:31:54.849Z
Learning: When reviewing Python test code in netbox_librenms_plugin/tests, treat “develop-owned” scaffold lines as off-limits for in-PR rewrites. A line is “develop-owned” if `git blame` for that line attributes it to a commit that is an ancestor of `origin/develop` (i.e., the commit is contained in `origin/develop`). For such lines, reviewers should acknowledge the findings as valid but defer the change by creating/using a follow-up issue targeting the `develop` branch (e.g., `#112`), rather than requesting modifications in the current feature/PR stack.

Applied to files:

  • netbox_librenms_plugin/tests/test_vlan_sync_concurrency.py
  • netbox_librenms_plugin/tests/test_validation_template_server_key.py
  • netbox_librenms_plugin/tests/test_coverage_sync_interfaces.py
📚 Learning: 2026-06-19T14:03:09.440Z
Learnt from: marcinpsk
Repo: marcinpsk/netbox-librenms-plugin PR: 113
File: netbox_librenms_plugin/tests/test_coverage_base_views2.py:555-567
Timestamp: 2026-06-19T14:03:09.440Z
Learning: In tests under netbox_librenms_plugin/tests, don’t rely on “pure” MagicMock setups that stub chained calls like `interfaces.filter.return_value.first.return_value` when the code under test is supposed to distinguish between (1) a librenms_id custom-field lookup and (2) a name-based fallback lookup. If the mock returns the same interface regardless of filter arguments, the test cannot detect which lookup path matched (renaming variables like `remote_port` doesn’t fix this). Use a real-DB hardening test instead: create/seed a `remote_port` value that is deliberately different from the actual interface name so only the librenms_id CF lookup can produce a match. If an existing MagicMock-masked test file is develop-inherited (identical on origin/develop), don’t modify it in feature PRs; add a new real-DB hardening test file (e.g., `test_enrich_remote_port_realdb.py`) on the develop-targeted branch.

Applied to files:

  • netbox_librenms_plugin/tests/test_vlan_sync_concurrency.py
  • netbox_librenms_plugin/tests/test_validation_template_server_key.py
  • netbox_librenms_plugin/tests/test_coverage_sync_interfaces.py
📚 Learning: 2026-06-25T07:07:59.192Z
Learnt from: marcinpsk
Repo: marcinpsk/netbox-librenms-plugin PR: 114
File: netbox_librenms_plugin/tests/test_oob_sync_review_fixes.py:0-0
Timestamp: 2026-06-25T07:07:59.192Z
Learning: When writing/adjusting tests (and any review/test helper code) that interact with NetBox’s custom User model, do not assume the user model has an `is_staff` field. If you need to check or set user privileges/eligibility, use fields that are known to exist on the NetBox User model (e.g., `is_superuser` and `is_active`) instead. Avoid setting `is_staff` (it can raise Django `FieldError` if that field doesn’t exist on the custom model).

Applied to files:

  • netbox_librenms_plugin/tests/test_vlan_sync_concurrency.py
  • netbox_librenms_plugin/tests/test_validation_template_server_key.py
  • netbox_librenms_plugin/tests/test_coverage_sync_interfaces.py
📚 Learning: 2026-07-02T21:46:46.384Z
Learnt from: marcinpsk
Repo: marcinpsk/netbox-librenms-plugin PR: 116
File: netbox_librenms_plugin/tests/test_coverage_bulk_import.py:132-140
Timestamp: 2026-07-02T21:46:46.384Z
Learning: In netbox_librenms_plugin/tests, keep the `_stub_norm_preload` autouse fixture (which patches `netbox_librenms_plugin.import_utils.bulk_import.preload_normalization_rules` to return `{}`) intentionally duplicated per mock-based test class/module. Do NOT hoist it into a global `conftest.py` autouse fixture, because that would apply the patch repo-wide and mask the real `preload_normalization_rules` behavior in real-DB tests that are meant to exercise it (see issue `#90`). If you need this patch, scope it to the specific mock-based tests that should stub normalization rules.

Applied to files:

  • netbox_librenms_plugin/tests/test_vlan_sync_concurrency.py
  • netbox_librenms_plugin/tests/test_validation_template_server_key.py
  • netbox_librenms_plugin/tests/test_coverage_sync_interfaces.py
📚 Learning: 2026-04-01T15:55:42.180Z
Learnt from: marcinpsk
Repo: marcinpsk/netbox-librenms-plugin PR: 50
File: netbox_librenms_plugin/tests/test_coverage_actions.py:88-171
Timestamp: 2026-04-01T15:55:42.180Z
Learning: When unit/integration testing actions that indirectly use a function imported at module import time, patch the function where it is *used* (the consumer’s import path), e.g. `netbox_librenms_plugin.views.imports.actions.resolve_naming_preferences`, rather than its original definition. For tests that target the function itself directly, patch the original dependency/definition (e.g. `netbox_librenms_plugin.utils.get_user_pref` or patch `resolve_naming_preferences` at `netbox_librenms_plugin.utils`) so the function under test sees the mocked behavior.

Applied to files:

  • netbox_librenms_plugin/tests/test_vlan_sync_concurrency.py
  • netbox_librenms_plugin/tests/test_validation_template_server_key.py
  • netbox_librenms_plugin/tests/test_coverage_sync_interfaces.py
📚 Learning: 2026-05-05T09:46:17.700Z
Learnt from: marcinpsk
Repo: marcinpsk/netbox-librenms-plugin PR: 58
File: netbox_librenms_plugin/tests/test_vm_operations.py:46-46
Timestamp: 2026-05-05T09:46:17.700Z
Learning: When the code under test performs *lazy imports* inside function bodies (i.e., the imported symbol is not bound at the module scope), mock/patch the *source module path that the function imports from*, not the consumer module path. The correct patch target is where the imported name is resolved at runtime (e.g., `virtualization.models.VirtualMachine`), because patching `netbox_librenms_plugin.import_utils.vm_operations.VirtualMachine` can fail with `AttributeError` since `VirtualMachine` is never a `vm_operations` module attribute.

Applied to files:

  • netbox_librenms_plugin/tests/test_vlan_sync_concurrency.py
  • netbox_librenms_plugin/tests/test_validation_template_server_key.py
  • netbox_librenms_plugin/tests/test_coverage_sync_interfaces.py
🪛 ast-grep (0.45.0)
netbox_librenms_plugin/tests/test_validation_template_server_key.py

[warning] 27-27: Regex pattern passed to re is built from a non-literal (variable, call, concatenation, or f-string) value. If that value is attacker-controlled it can introduce a malicious pattern with catastrophic backtracking (ReDoS). Use a hardcoded literal pattern, or validate/escape untrusted input with re.escape() and bound the regex complexity before compiling.
Context: re.search(name_pattern, tag, re.IGNORECASE)
Note: [CWE-1333] Inefficient Regular Expression Complexity.

(redos-non-literal-regex-python)

🔇 Additional comments (3)
netbox_librenms_plugin/tests/test_vlan_sync_concurrency.py (1)

1-96: LGTM!

netbox_librenms_plugin/tests/test_coverage_sync_interfaces.py (2)

12-39: LGTM!

Also applies to: 91-94, 104-107, 376-376, 393-403, 442-456, 474-491, 510-527, 547-574, 597-600, 627-629, 721-784, 847-848, 852-883


895-895: 📐 Maintainability & Code Quality

No change needed.

Comment on lines +45 to +46
def _form_has_named_control(form_html, name):
return next(_named_control_tags(form_html, name), None) is not None

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Require a hidden input for server_key.

_form_has_named_control() accepts <button name="server_key">. A button submits its value only when that button is clicked. Lines 73 and 83 can then pass although another action does not post server_key.

Add a server-key-specific matcher that requires an <input type="hidden" name="server_key">. Keep button matching only for action.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@netbox_librenms_plugin/tests/test_validation_template_server_key.py` around
lines 45 - 46, Update the validation helpers around _form_has_named_control so
server_key validation only matches an input element with type="hidden" and
name="server_key", rather than any named control such as a button. Preserve
button matching for action validation, and use the server-key-specific matcher
at the checks on lines 73 and 83.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant