From 012bcfe5256c684cb8bc3845453458340f3ddf49 Mon Sep 17 00:00:00 2001 From: "spyroot@gmail.com" Date: Sat, 25 Jul 2026 04:06:17 +0400 Subject: [PATCH] Serve TestNetworkShare as sync in the mock and converge its dualmode suite DellLCService.TestNetworkShare realizes synchronously: Dell's API guide documents no 202/job for it (while sibling LC actions document async explicitly) and Dell's own tooling treats the POST as terminal. Teach MockRedfishService a documented-sync exception (200 + Base success, no task) and convert the last hand-rolled dualmode scaffold to the shared mock, asserting no task_id is fabricated. --- tests/conftest.py | 28 +++++ ...est_dell_lc_network_share_test_dualmode.py | 112 ++++++++---------- 2 files changed, 77 insertions(+), 63 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 0dff5f39..d6ef06f3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -150,6 +150,22 @@ def _vendor_fixture_dir(vendor): _DELL_JOB_ID = "JID_000000000001" # Dell OEM DellJobService job id _DMTF_TASK_ID = "1" # DMTF TaskService monitor id (all non-Dell) +# Actions with a DOCUMENTED synchronous realization: the vendor answers the POST +# terminally (200 + a Base success message), never with a 202 task/job. Serving +# the blanket 202+Location for these would fabricate an async realization the +# vendor does not have — the same wrong-lens hazard as SubmitTestEvent below. +# DellLCService.TestNetworkShare evidence (no live Dell box exists to trace — +# the XR8620t corpus donor is decommissioned): +# * iDRAC9 Redfish API Guide 4.20.20.20 pp.107-108: TestNetworkShare documents +# no 202 row and no job language, while sibling DellLCService actions document +# async explicitly (SupportAssistUploadLastCollection lists 202; SystemErase +# says "a job ID is returned"). +# * Dell's Ansible module idrac_diagnostics.py: test_network_share() POSTs with +# no job wait and parses errors as a sync @Message.ExtendedInfo body, while +# RunePSADiagnostics in the same file gets an explicit job wait. +# * Dell's TestNetworkShareREDFISH.py: terminal at the POST, never polls a job. +_SYNC_ACTION_SUFFIXES = ("delllcservice.testnetworkshare",) + def _vendor_family(vendor): """Collapse a fixture-set name to its vendor family. @@ -200,6 +216,8 @@ class MockRedfishService: * PATCH -> deep-merges the body into state, 200 + a success message * POST -> protocol-accurate per shape: ``SubmitTestEvent`` -> 204 (sync); subscription create (``/Subscriptions``) -> 201 + ``Location``; + documented-sync actions (``_SYNC_ACTION_SUFFIXES``) -> 200 + a + Base success message, no task; other ``/Actions/`` -> 202 with a ``Location`` task header; else 204. The 202 task id is VENDOR-FAITHFUL: ``vendor="dell"`` returns an OEM ``JID_`` job; every other vendor a plain DMTF TaskService id (never @@ -298,6 +316,16 @@ def post_cb(self, request, context): context.status_code = 201 context.headers["Location"] = request.path.rstrip("/") + "/1" return "" + if path.rstrip("/").endswith(_SYNC_ACTION_SUFFIXES): + # Documented-sync actions (see _SYNC_ACTION_SUFFIXES) realize in + # place: 200 + a Base success message, no task and no Location. + import json + context.status_code = 200 + return json.dumps( + {"@Message.ExtendedInfo": [{"MessageId": "Base.1.12.Success", + "Message": "Successfully Completed Request", + "Severity": "OK"}]} + ) if "/actions/" in path: # A vendor that realizes an Action as a task returns 202 + a Location # header at the new task. The id is vendor-faithful (Dell JID_ OEM job diff --git a/tests/dell_lc/test_dell_lc_network_share_test_dualmode.py b/tests/dell_lc/test_dell_lc_network_share_test_dualmode.py index 4a672a78..3cf6e8a5 100644 --- a/tests/dell_lc/test_dell_lc_network_share_test_dualmode.py +++ b/tests/dell_lc/test_dell_lc_network_share_test_dualmode.py @@ -1,8 +1,8 @@ """Dual-mode-style coverage for DellLCService.TestNetworkShare.""" -import json from pathlib import Path import pytest +from conftest import MockRedfishService, _build_fixture_index from vendor_corpus import corpus_dir from redfish_ctl.cmd_exceptions import InvalidArgument @@ -17,77 +17,58 @@ Path(__file__).parent.parent / "dell_xr8620t_corpus.tar.gz", "10.252.252.209", ) -DELL_INDEX = {path.name.lower(): path for path in DELL_CORPUS.glob("*.json")} SERVICE_URI = "/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/DellLCService" TARGET_URI = f"{SERVICE_URI}/Actions/DellLCService.TestNetworkShare" -def _fixture_for_path(path): - """Return the extracted Dell fixture matching a Redfish path. - - :param path: requests-mock request path. - :return: fixture path, or None when the corpus lacks the resource. - """ - name = "_" + path.strip("/").replace("/", "_") + ".json" - return DELL_INDEX.get(name.lower()) - - @pytest.fixture -def dell_lc_manager(): - """Serve the committed Dell corpus over requests-mock. +def dell_lc_mock(): + """Return a manager and mock service backed by the Dell XR8620t corpus. + + TestNetworkShare is a documented-sync action: the vendor-faithful service + answers its POST with 200 plus a Base success message and never a task + (see ``_SYNC_ACTION_SUFFIXES`` in conftest for the Dell evidence). - :return: tuple of IDracManager and recorded requests. + :return: tuple of IDracManager and the recording MockRedfishService. """ requests_mock = pytest.importorskip("requests_mock") - requests = [] - - def get_cb(request, context): - requests.append(request) - fixture = _fixture_for_path(request.path) - if fixture is None: - context.status_code = 404 - return json.dumps({"error": f"no fixture for {request.path}"}) - context.status_code = 200 - return fixture.read_text() - - def post_cb(request, context): - requests.append(request) - context.status_code = 200 - return json.dumps({ - "@Message.ExtendedInfo": [{ - "MessageId": "Base.1.12.Success", - "Message": "Successfully Completed Request", - "Severity": "OK", - }] - }) - + service = MockRedfishService( + DELL_CORPUS, + index=_build_fixture_index(DELL_CORPUS), + vendor="dell", + ) with requests_mock.Mocker() as mocker: - mocker.get(requests_mock.ANY, text=get_cb) - mocker.post(requests_mock.ANY, text=post_cb) - manager = IDracManager( - idrac_ip="mock-dell-lc", - idrac_username="root", - idrac_password="mock", - insecure=True, - is_debug=False, + mocker.get(requests_mock.ANY, text=service.get_cb) + mocker.patch(requests_mock.ANY, text=service.patch_cb) + mocker.post(requests_mock.ANY, text=service.post_cb) + mocker.delete(requests_mock.ANY, text=service.delete_cb) + service.mocker = mocker + yield ( + IDracManager( + idrac_ip="mock-dell-lc", + idrac_username="root", + idrac_password="mock", + insecure=True, + is_debug=False, + ), + service, ) - yield manager, requests -def _post_requests(requests): - """Return POST requests recorded by the mock Redfish transport. +def _post_requests(service): + """Return POST requests recorded by the mock Redfish service. - :param requests: recorded requests-mock request objects. + :param service: the recording MockRedfishService. :return: list of POST requests. """ - return [request for request in requests if request.method == "POST"] + return [request for request in service.requests if request.method == "POST"] def test_dell_lc_network_share_test_lists_target_without_mutating( - dell_lc_manager, + dell_lc_mock, ): """Without --host, the command lists the discovered action target only.""" - manager, requests = dell_lc_manager + manager, service = dell_lc_mock result = manager.sync_invoke( ApiRequestType.DellLcNetworkShareTest, @@ -107,14 +88,14 @@ def test_dell_lc_network_share_test_lists_target_without_mutating( "NFS", "TFTP", ] - assert _post_requests(requests) == [] + assert _post_requests(service) == [] def test_dell_lc_network_share_test_without_confirm_is_preview_only( - dell_lc_manager, + dell_lc_mock, ): """A host payload is resolved but not POSTed unless --confirm is present.""" - manager, requests = dell_lc_manager + manager, service = dell_lc_mock result = manager.sync_invoke( ApiRequestType.DellLcNetworkShareTest, @@ -135,12 +116,16 @@ def test_dell_lc_network_share_test_without_confirm_is_preview_only( "ProxySupport": "Off", "IgnoreCertWarning": "On", } - assert _post_requests(requests) == [] + assert _post_requests(service) == [] -def test_dell_lc_network_share_test_confirm_posts_payload(dell_lc_manager): - """--confirm POSTs TestNetworkShare to the discovered action target.""" - manager, requests = dell_lc_manager +def test_dell_lc_network_share_test_confirm_posts_payload(dell_lc_mock): + """--confirm POSTs TestNetworkShare; the action realizes synchronously. + + The Dell-faithful answer is a terminal 200 success, so the result must + carry no fabricated ``task_id`` (the realization is sync, not a job). + """ + manager, service = dell_lc_mock result = manager.sync_invoke( ApiRequestType.DellLcNetworkShareTest, @@ -152,13 +137,14 @@ def test_dell_lc_network_share_test_confirm_posts_payload(dell_lc_manager): confirm=True, ) - posts = _post_requests(requests) + posts = _post_requests(service) assert isinstance(result, CommandResult) assert result.error is None assert result.data["executed"] is True assert result.data["action"] == "#DellLCService.TestNetworkShare" assert result.data["target"] == TARGET_URI assert result.data["level"] == "reversible" + assert "task_id" not in result.data assert len(posts) == 1 assert posts[0].path.lower() == TARGET_URI.lower() assert posts[0].json() == { @@ -170,10 +156,10 @@ def test_dell_lc_network_share_test_confirm_posts_payload(dell_lc_manager): def test_dell_lc_network_share_test_rejects_invalid_share_type( - dell_lc_manager, + dell_lc_mock, ): """Inline allowable values reject an unsupported ShareType before POST.""" - manager, requests = dell_lc_manager + manager, service = dell_lc_mock result = manager.sync_invoke( ApiRequestType.DellLcNetworkShareTest, @@ -193,7 +179,7 @@ def test_dell_lc_network_share_test_rejects_invalid_share_type( "value": "Local", "allowed": ["CIFS", "FTP", "HTTP", "HTTPS", "NFS", "TFTP"], }] - assert _post_requests(requests) == [] + assert _post_requests(service) == [] def test_dell_lc_network_share_test_requires_nonempty_host():