Add unit tests for osism/tasks/netbox.py#2458
Conversation
There was a problem hiding this comment.
Hey - I've left some high level feedback:
- In the
_update_netbox_device_fieldtests (e.g.test_update_field_updates_and_savesand the request‑error tests), thenbMagicMock doesn’t have abase_urlattribute even though the implementation callscreate_netbox_semaphore(nb.base_url); consider reusing themock_nbfixture or explicitly settingbase_urlthere to avoid brittle tests if the code starts relying on it more strictly. - The semaphore behavior in
_update_netbox_device_fieldis 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.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>
36642e6 to
6bca113
Compare
| 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") |
There was a problem hiding this comment.
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"]) |
There was a problem hiding this comment.
"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( |
There was a problem hiding this comment.
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.
Adds
tests/unit/tasks/test_netbox.pycovering the Celery tasks of thenetboxworker as specified in #2355:_update_netbox_device_field: happy path (custom-field update + save), device not found, all fourrequestsexception handlers (ConnectTimeout,ReadTimeout,ConnectionError,HTTPError), errors raised bydevice.save(), and the semaphore being created fromnb.base_urland entered/exited around the API call._matches_netbox_filter: empty filter,primaryfilter variants (case-insensitive, substring), URL/name/site matching, missingnetbox_name/netbox_siteattributes, and the no-match fall-through.set_maintenance/set_provision_state/set_power_state: one parametrised body over the three tasks (same approach asQUERY_LIST_VARIANTSin the conductor tests) covering Redlock key/timeouts, unacquired lock, failed update still returningTrue, filter-based primary/secondary skipping, theutils.secondary_nb_listfallback, lock release on exceptions, theset_power_stateNone→"n/a"conversion, and task-lock abort before Redlock creation.get_location_id/get_rack_id(parametrised): found, missing, andValueErrorfrom ambiguous matches.get_*tasks: delegation to the expected pynetbox endpoints.manage: exactnetbox-managerenvironment withstr()-coerced settings, argument/keyword forwarding torun_command, return value pass-through, and no command execution when tasks are locked.pingandsetup_periodic_tasks.utils.nbandutils.secondary_nb_listare lazy attributes resolved viaosism.utils.__getattr__; they are always patched withcreate=Trueso no real NetBox connection is ever opened. All tasks are invoked directly — Celery bindsselfon direct calls, so no broker is needed.Closes #2355