diff --git a/assets/sapcli/commands/crud.py b/assets/sapcli/commands/crud.py index 4860d20..66d876e 100644 --- a/assets/sapcli/commands/crud.py +++ b/assets/sapcli/commands/crud.py @@ -474,7 +474,10 @@ def cmd_info(args: argparse.Namespace, client: ADTClient) -> None: print(f" ✗ 对象不存在: {parsed.display_name}") raise ObjectNotFoundError(parsed.display_name, obj_type) if resp.status_code == 406: - hdrs["Accept"] = "application/xml" + # NW 7.40 实测:DDIC 专属媒体类型返回 406,且 application/xml 本身也 406 + # (structure 专属头 → 406、application/xml → 406、*/* → 200)。 + # 回退必须是 */*,否则回退恒败、命令报「查询失败: HTTP 406」。 + hdrs["Accept"] = "*/*" resp = client.session.get(url, headers=hdrs) if resp.status_code != 200: print(f" ✗ 查询失败: HTTP {resp.status_code}") diff --git a/tests/unit/test_commands.py b/tests/unit/test_commands.py index c8cfd48..3a408d0 100644 --- a/tests/unit/test_commands.py +++ b/tests/unit/test_commands.py @@ -36,9 +36,13 @@ def _make_client(): client.user = "TESTUSER" # client.session 用于直接 HTTP 调用 client.session = MagicMock() - client._headers.return_value = { - "content-type": "application/xml", + # 真实 _headers() 每次返回**新** dict。用 return_value 会让所有被记录的 + # kwargs["headers"] 指向同一对象——后续赋值会覆盖历史值,令 header 断言失真 + # (实测:406 回退的 Accept 断言曾被此别名效应蒙过)。 + client._headers.side_effect = lambda content_type="application/xml": { + "content-type": content_type, "x-csrf-token": "test-csrf-token", + "Accept": "*/*", } return client diff --git a/tests/unit/test_commands_extra.py b/tests/unit/test_commands_extra.py index 08b2b7f..b39dfa8 100644 --- a/tests/unit/test_commands_extra.py +++ b/tests/unit/test_commands_extra.py @@ -37,9 +37,13 @@ def _make_client(): client.csrf_token = "test-csrf-token" client.user = "TESTUSER" client.session = MagicMock() - client._headers.return_value = { - "content-type": "application/xml", + # 真实 _headers() 每次返回**新** dict。用 return_value 会让所有被记录的 + # kwargs["headers"] 指向同一对象——后续赋值会覆盖历史值,令 header 断言失真 + # (实测:406 回退的 Accept 断言曾被此别名效应蒙过)。 + client._headers.side_effect = lambda content_type="application/xml": { + "content-type": content_type, "x-csrf-token": "test-csrf-token", + "Accept": "*/*", } return client @@ -397,7 +401,11 @@ class TestCmdInfoObjectNotFound(unittest.TestCase): self.assertIn("不匹配", str(ctx.exception)) def test_info_http_406_fallback(self): - """HTTP 406 时回退到 application/xml。""" + """HTTP 406 时回退到 */*。 + + 只断言「重试了一次」会漏掉回退值本身无效的情况——实测 NW 7.40 上 + application/xml 同样返回 406,回退恒败。故必须断言回退用的 Accept 值。 + """ from sapcli.commands.crud import cmd_info client = _make_client() @@ -406,10 +414,39 @@ class TestCmdInfoObjectNotFound(unittest.TestCase): _mock_resp(406), _mock_resp(200, content=_INFO_XML), ] - # 应该不抛异常 cmd_info(_args(), client) self.assertEqual(client.session.get.call_count, 2) + def test_info_406_fallback_accept_is_wildcard(self): + """回退请求的 Accept 必须是 */*(NW 7.40 上唯一可用值)。 + + 实测(S4T / NW 7.40):structure 专属头 → 406,application/xml → 406,*/* → 200。 + 即 application/xml 本身也 406,用它做回退等于不重试。 + + ⚠️ 断言必须在**请求发生时快照** headers:cmd_info 重试时复用同一个 hdrs dict, + 直接读 call_args_list[i].kwargs["headers"] 会因别名而全部显示最后一次赋的值, + 使「回退换了头」这类断言永远无法成立(也无法发现回退值无效)。 + """ + from sapcli.commands.crud import cmd_info + + client = _make_client() + seen = [] + + def _get(url, headers=None, **kw): + seen.append(dict(headers or {})) # 快照,断开别名 + return _mock_resp(406) if len(seen) == 1 else _mock_resp(200, content=_INFO_XML) + + client.session.get.side_effect = _get + with patch("builtins.print"): + cmd_info(_args(), client) + + self.assertEqual(len(seen), 2, "应为「原始请求 + 回退请求」两次") + self.assertEqual(seen[1]["Accept"], "*/*") + self.assertNotEqual(seen[1]["Accept"], "application/xml", + "application/xml 在 NW 7.40 上同样返回 406,不能作为回退值") + self.assertNotEqual(seen[0]["Accept"], seen[1]["Accept"], + "回退请求必须换用不同的 Accept 头") + # ═══════════════════════════════════════════════════════════════ # crud.py — cmd_info — 传输请求号字段(纯只读查 E071) diff --git a/tests/unit/test_crud_extra.py b/tests/unit/test_crud_extra.py index d8133c8..e6a681c 100644 --- a/tests/unit/test_crud_extra.py +++ b/tests/unit/test_crud_extra.py @@ -46,9 +46,13 @@ def _make_client(): client.csrf_token = "test-csrf-token" client.user = "TESTUSER" client.session = MagicMock() - client._headers.return_value = { - "content-type": "application/xml", + # 真实 _headers() 每次返回**新** dict。用 return_value 会让所有被记录的 + # kwargs["headers"] 指向同一对象——后续赋值会覆盖历史值,令 header 断言失真 + # (实测:406 回退的 Accept 断言曾被此别名效应蒙过)。 + client._headers.side_effect = lambda content_type="application/xml": { + "content-type": content_type, "x-csrf-token": "test-csrf-token", + "Accept": "*/*", } return client