Add unit tests for osism/api.py helpers and Pydantic models#2463
Add unit tests for osism/api.py helpers and Pydantic models#2463berendt wants to merge 2 commits into
Conversation
bdcace2 to
03dbbaa
Compare
Dismissing — this was meant to be a pending/draft review, not a submitted one. Reposting as a draft.
| assert api._mask_inventory_secrets({"key": value}) == {"key": value} | ||
|
|
||
|
|
||
| def test_mask_inventory_secrets_does_not_traverse_lists(): |
There was a problem hiding this comment.
This test asserts, as expected behavior, that a secret nested inside a list is returned unmasked:
data = {"hosts": [{"db_password": "hunter2"}]}
assert api._mask_inventory_secrets(data) == {"hosts": [{"db_password": "hunter2"}]}That codifies a production leak. _mask_inventory_secrets (osism/api.py:35-51) recurses into dict but has no list branch, and it masks the ansible-inventory --host output returned by the API at api.py:833, 903, and 1104. Ansible hostvars routinely contain lists of dicts (e.g. users: [{name: admin, password: ...}]), so any secret nested in a list is returned verbatim through /hostvars and the search endpoints — the masking layer that exists to prevent exactly this is silently bypassed.
The sibling masker _mask_secrets_inplace (osism/tasks/conductor/utils.py:314-320) already has an isinstance(obj, list) branch, so the missing list handling here is an oversight, not an intentional design choice.
Fix _mask_inventory_secrets to traverse lists (mirroring _mask_secrets_inplace), and flip this assertion to expect {"hosts": [{"db_password": "***"}]}. Keep the production fix in its own commit, separate from the test changes. As written, the test would actively resist that fix.
_mask_inventory_secrets() recursed into dicts but not into lists, so
secrets nested inside lists (for example users: [{password: ...}] in
Ansible hostvars) were returned unmasked by the /hostvars and search
endpoints. Traverse lists as well and mask secret keys and Ansible
Vault encrypted values at any nesting depth, mirroring
_mask_secrets_inplace() in osism/tasks/conductor/utils.py.
Assisted-by: Claude:claude-fable-5
Signed-off-by: Christian Berendt <berendt@osism.tech>
Cover the module-level helper functions and request/response models of the OSISM API server in a new tests/unit/test_api_helpers.py: - _mask_inventory_secrets(): secret-key matching (password/secret substrings, ironic_osism_ prefix), Ansible Vault values including leading whitespace, recursion into nested dicts and lists (including Vault values inside lists), pass-through of non-dict/non-string values, non-string keys, and input immutability - find_device_by_identifier(): lookup precedence name -> cf_inventory_hostname -> serial, early return without a NetBox connection, first-match semantics - process_netbox_webhook(): reconciler.run.delay() dispatch for managed servers, log-only branches for switches and interfaces, device_type fallback to "node", unknown URL early return, unmanaged devices, KeyError propagation for incomplete payloads - LogConfig: defaults, model_dump() structure, dictConfig compatibility - Pydantic models: validation, coercion (UUID, datetime, nested models), defaults and default_factory independence, required-field enforcement, model_dump() round trips Endpoint behavior (HTTP status codes, masking in responses) is covered separately with the FastAPI TestClient. Closes #2359 Assisted-by: Claude:claude-fable-5 Signed-off-by: Christian Berendt <berendt@osism.tech>
03dbbaa to
e6a91ed
Compare
Adds
tests/unit/test_api_helpers.pycovering the module-level helpers and Pydantic models of the OSISM API server (osism/api.py), as specified in #2359:_mask_inventory_secrets()— secret-key matching (password/secretsubstrings,ironic_osism_prefix, using the real_is_secret_key), Ansible Vault values (including leading whitespace, and the marker mid-string staying unchanged), nested dict recursion, secret keys with dict values masked as a whole, pass-through of non-dict/non-string values and non-string keys, non-traversal of lists, empty dict, input immutability.find_device_by_identifier()— lookup precedencename→cf_inventory_hostname→serialwith call-order assertions, earlyNonereturn without a NetBox connection, first-match semantics for multiple results.utils.nbis injected viapatch.dict("osism.utils.__dict__", ...)to avoid triggering the lazy module__getattr__, matching the existing pattern intests/unit/commands/.process_netbox_webhook()—reconciler.run.delay()dispatched exactly once for managed servers; no dispatch for switches, interfaces (device fetched vianb.dcim.devices.get(id=...), asserted),device_typefallback to"node", unknown URLs (early return without NetBox access), and unmanaged devices;KeyErrorpropagation for payloads withouturl/name.LogConfig— defaults,model_dump()structure (formatter,StreamHandleronext://sys.stderr, all five loggers),dictConfigcompatibility.NotificationBaremetalandWebhookNetboxDatavalidation and coercion (UUID, datetime, per-field required checks),BaremetalNodeoptional fields anddefault_factoryindependence, all-optional models, and parametrized required-field enforcement plusmodel_dump()round trips for the response models, including dict-to-BaremetalNodeNetboxInfocoercion andAny-typed value fields.Endpoint behavior (HTTP status codes, masking applied in responses) is out of scope here and belongs to the companion endpoint issue (Tier 7, split 2/2).
Closes #2359
🤖 Generated with Claude Code