From 86519c51d6ebd59a26a1eae9fb246d099271a361 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B4=E8=AE=A9=E5=AE=87?= Date: Fri, 11 Sep 2026 00:53:02 +0800 Subject: [PATCH] =?UTF-8?q?fix(info):=20406=20=E5=9B=9E=E9=80=80=E5=80=BC?= =?UTF-8?q?=E6=94=B9=E4=B8=BA=20*/*=20=E2=80=94=E2=80=94=20=E5=8E=9F?= =?UTF-8?q?=E5=80=BC=20application/xml=20=E6=9C=AC=E8=BA=AB=E4=B9=9F?= =?UTF-8?q?=E8=BF=94=E5=9B=9E=20406?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `info` 在 NW 7.40 上对 DDIC 类型恒报「查询失败: HTTP 406」,实测根因: 回退分支把 Accept 从专属媒体类型换成 `application/xml`,而该值在本系统同样返回 406, 等于没回退。真实客户端 `_headers()` 的 Accept 默认值本就是 `*/*`,此处相当于 把可用默认值覆盖成了失效值。 实测(S4T / NW 7.40,structure ZMM_BIP_001_HEADER): application/vnd.sap.adt.ddic.structures.v1|v2|v3+xml → 406 application/xml → 406 */* → 200 修后真机验证:`info --name ZMM_BIP_001_HEADER --type structure` 查询成功。 已知限制(非本修复引入):NW 7.40 的 DDIC 端点在 */* 下只返回 blueSource, 不含 description/packageRef,故 structure/tabletype 等类型的「描述/开发包/负责人」 为空。需这些字段时用 read-table 查 DD02T(描述)/ TADIR(开发包)。 (objectproperties 端点在本系统返回 422,不可用。) 测试: - 新增 test_info_406_fallback_accept_is_wildcard —— 断言回退 Accept 必须为 */*, 且明确排除 application/xml。**必须用 side_effect 在请求发生时快照 headers**: cmd_info 重试时复用同一个 hdrs dict,直接读 call_args_list[i].kwargs["headers"] 会因别名而全部显示最后一次赋的值,令该断言永远无法成立。 - 三个测试文件的 _make_client() 由 `_headers.return_value={...}`(共享 dict) 改为 side_effect 返回新 dict,与真实实现一致——原写法会让被记录的 headers 全部指向同一对象,使 header 断言失真。 - 反向验证:把回退改回 application/xml → 测试报红 AssertionError('application/xml' != '*/*')。 699 tests OK。 --- assets/sapcli/commands/crud.py | 5 +++- tests/unit/test_commands.py | 8 ++++-- tests/unit/test_commands_extra.py | 45 ++++++++++++++++++++++++++++--- tests/unit/test_crud_extra.py | 8 ++++-- 4 files changed, 57 insertions(+), 9 deletions(-) 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