Skip to content

Add unit tests for osism/tasks/netbox.py#2458

Open
berendt wants to merge 1 commit into
mainfrom
unit-tests-tasks-netbox
Open

Add unit tests for osism/tasks/netbox.py#2458
berendt wants to merge 1 commit into
mainfrom
unit-tests-tasks-netbox

Conversation

@berendt

@berendt berendt commented Jul 13, 2026

Copy link
Copy Markdown
Member

Adds tests/unit/tasks/test_netbox.py covering the Celery tasks of the netbox worker as specified in #2355:

  • _update_netbox_device_field: happy path (custom-field update + save), device not found, all four requests exception handlers (ConnectTimeout, ReadTimeout, ConnectionError, HTTPError), errors raised by device.save(), and the semaphore being created from nb.base_url and entered/exited around the API call.
  • _matches_netbox_filter: empty filter, primary filter variants (case-insensitive, substring), URL/name/site matching, missing netbox_name/netbox_site attributes, and the no-match fall-through.
  • set_maintenance / set_provision_state / set_power_state: one parametrised body over the three tasks (same approach as QUERY_LIST_VARIANTS in the conductor tests) covering Redlock key/timeouts, unacquired lock, failed update still returning True, filter-based primary/secondary skipping, the utils.secondary_nb_list fallback, lock release on exceptions, the set_power_state None"n/a" conversion, and task-lock abort before Redlock creation.
  • get_location_id / get_rack_id (parametrised): found, missing, and ValueError from ambiguous matches.
  • The four pass-through get_* tasks: delegation to the expected pynetbox endpoints.
  • manage: exact netbox-manager environment with str()-coerced settings, argument/keyword forwarding to run_command, return value pass-through, and no command execution when tasks are locked.
  • ping and setup_periodic_tasks.

utils.nb and utils.secondary_nb_list are lazy attributes resolved via osism.utils.__getattr__; they are always patched with create=True so no real NetBox connection is ever opened. All tasks are invoked directly — Celery binds self on direct calls, so no broker is needed.

Closes #2355

@sourcery-ai sourcery-ai 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.

Hey - I've left some high level feedback:

  • In the _update_netbox_device_field tests (e.g. test_update_field_updates_and_saves and the request‑error tests), the nb MagicMock doesn’t have a base_url attribute even though the implementation calls create_netbox_semaphore(nb.base_url); consider reusing the mock_nb fixture or explicitly setting base_url there to avoid brittle tests if the code starts relying on it more strictly.
  • The semaphore behavior in _update_netbox_device_field is only asserted for the happy path; it may be worth adding a case where the NetBox call raises to confirm the semaphore context manager is exited correctly even on exceptions.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In the `_update_netbox_device_field` tests (e.g. `test_update_field_updates_and_saves` and the request‑error tests), the `nb` MagicMock doesn’t have a `base_url` attribute even though the implementation calls `create_netbox_semaphore(nb.base_url)`; consider reusing the `mock_nb` fixture or explicitly setting `base_url` there to avoid brittle tests if the code starts relying on it more strictly.
- The semaphore behavior in `_update_netbox_device_field` is only asserted for the happy path; it may be worth adding a case where the NetBox call raises to confirm the semaphore context manager is exited correctly even on exceptions.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Covers the Celery tasks of the netbox worker:

- _update_netbox_device_field: successful custom-field update and
  save; device not found returns False without an error log; the
  four requests exception handlers (ConnectTimeout, ReadTimeout via
  the Timeout handler, ConnectionError, HTTPError as generic
  RequestException) each return False and log the matching message,
  including the base_url of the instance; errors raised by
  device.save() take the same path; the NetBox semaphore is created
  from nb.base_url and entered/exited around the API call, and is
  exited both when a requests exception is handled inside the with
  block and when an unexpected exception propagates out of it.

- _matches_netbox_filter: empty/None filter matches everything;
  'primary' filter variants (case-insensitive, substring) against
  primary and secondary instances; case-insensitive URL substring
  match; instances without netbox_name/netbox_site attributes fall
  through without AttributeError; netbox_name and netbox_site
  matches; nothing matching returns False.

- set_maintenance / set_provision_state / set_power_state share one
  parametrised body (same approach as QUERY_LIST_VARIANTS in the
  conductor tests): the happy path asserts the Redlock key, auto
  release time, acquire timeout and the update call against the
  primary; an unacquired lock returns False without update or
  release; a failed update logs "Could not set ..." but the task
  still returns True; a non-matching filter skips the primary; only
  matching secondaries are processed; secondary_nb_list=None falls
  back to utils.secondary_nb_list; the lock is released when the
  update raises; set_power_state converts state=None to "n/a"; a
  task-lock SystemExit aborts before the Redlock is created.

- get_location_id / get_rack_id (parametrised): id returned when
  found, None when missing, None on ValueError from ambiguous
  matches.

- get_devices, get_device_by_name, get_interfaces_by_device and
  get_addresses_by_device_and_interface delegate to the expected
  pynetbox endpoints and pass the result through.

- manage: exact netbox-manager environment with str()-coerced
  settings values, command path, argument and keyword forwarding,
  run_command return value passed through, request id None on a
  direct call, and no run_command call when tasks are locked.

- ping returns utils.nb.status() unchanged; setup_periodic_tasks is
  a no-op.

utils.nb and utils.secondary_nb_list are lazy attributes resolved
via osism.utils.__getattr__ and are always patched with create=True
so no real NetBox connection is ever opened.

Closes #2355

Assisted-by: Claude:claude-fable-5
Signed-off-by: Christian Berendt <berendt@osism.tech>
@berendt berendt force-pushed the unit-tests-tasks-netbox branch from 36642e6 to 6bca113 Compare July 13, 2026 20:38
@berendt berendt moved this from New to Ready for review in Human Board Jul 13, 2026
@berendt berendt requested a review from ideaship July 13, 2026 20:40
def test_get_addresses_by_device_and_interface_delegates_to_filter(mock_nb):
result = netbox.get_addresses_by_device_and_interface("n1", "eth0")

mock_nb.dcim.addresses.filter.assert_called_once_with(device="n1", interface="eth0")

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.

This assertion pins a broken pynetbox path. The production task at osism/tasks/netbox.py:380 calls utils.nb.dcim.addresses.filter(...), but pynetbox exposes IP addresses under ipam.ip_addresses, not dcim.addresses — see osism/tasks/conductor/netbox.py:122,177 in this same repo, which use utils.nb.ipam.ip_addresses.filter(...). Against a real NetBox this 404s; the full MagicMock hides that. Don't land a test that certifies the wrong endpoint: fix the production line to ipam.ip_addresses in a separate commit, then assert the corrected path here. (The device=/interface= filter keys are valid on that endpoint — both filter by name — so only the app/endpoint changes.)

assert netbox._matches_netbox_filter(nb, netbox_filter) is True


@pytest.mark.parametrize("netbox_filter", ["primary", "PRIMARY", "primary-dc"])

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.

"primary-dc" in this parametrize list cements a production bug. In _matches_netbox_filter (osism/tasks/netbox.py:83) the primary branch is "primary" in filter_lower — reversed from every other check, which does filter_lower in <field>. As a result any filter containing the substring "primary" (e.g. a secondary at site primary-region) also selects the primary and updates it unintentionally. Don't assert "primary-dc" matches here. Correct the production line (exact-keyword match, or filter_lower in "primary") in a separate commit, then update the test to match.



@pytest.mark.parametrize(SET_TASK_PARAMS, SET_TASK_VARIANTS)
def test_set_task_update_failure_logs_error_but_returns_true(

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.

This asserts result is True when the update failed, which cements a contract bug: all three set_* tasks fall through to return True (osism/tasks/netbox.py:183) even though _update_netbox_device_field returned False (lines 148-153 only log). The docstring — "True if lock was acquired and operation succeeded" (line 132) — establishes the intended contract, so this is a bug, not an ambiguity: a caller (e.g. conductor/ironic.py:1099) can't distinguish success from a silently-failed update. Fix the production code in a separate commit to aggregate update failures into a False return, and change this test to assert False on the failure path as part of that same fix. If the production change isn't happening here, drop this test rather than land one asserting True.

@github-project-automation github-project-automation Bot moved this from Ready for review to In review in Human Board Jul 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In review

Development

Successfully merging this pull request may close these issues.

Unit tests for osism/tasks/netbox.py

3 participants