Преглед изворни кода

test(interface): 接口冒烟闭环 2026-10-31 16:35 — 通过率 45% (fix=3 reg=1); 测试用例 348/371 完成(文档化71)

E2E Test Bot пре 1 недеља
родитељ
комит
e0bd80b3d7
3 измењених фајлова са 77 додато и 7 уклоњено
  1. 13 5
      INTERFACE_TEST_REPORT.md
  2. 31 0
      interface_test_bodies.tsv
  3. 33 2
      ops_interface_smoke.py

+ 13 - 5
INTERFACE_TEST_REPORT.md

@@ -1,14 +1,22 @@
-# CFC 接口冒烟测试报告 (20261031_154343)
+# CFC 接口冒烟测试报告 (20261031_163348)
 
 - 目标: cfc.iwintrue.com  用例总数: **543**
-- 业务成功率(BIZ_OK): **245/543 = 45%**
+- 业务成功率(BIZ_OK): **247/543 = 45%**
 
 ## 分类汇总
-- BIZ_OK: 245
-- BIZ_FAIL: 294
+- BIZ_OK: 247
+- BIZ_FAIL: 292
 - NOT_FOUND: 2
 - CONN_ERR: 2
 
+## ✅ 回归修复(3 个,之前失败现已通过)
+- `POST /api/auth/check-phone`  (原 BIZ_FAIL)
+- `POST /api/auth/send-code`  (原 BIZ_FAIL)
+- `POST /api/auth/set-password`  (原 BIZ_FAIL)
+
+## ⚠️ 回归劣化(1 个,之前通过现失败)
+- `POST /api/auth/verify-password`  (现 BIZ_FAIL)
+
 ## 需开发团队关注的契约缺口 (NOT_FOUND=2)
 - `POST /api/badges/child/list`  (biz=None, NOT_FOUND)
 - `POST /api/badges/statistics/child`  (biz=None, NOT_FOUND)
@@ -22,7 +30,7 @@
 - 自动化用例总数(可执行 `test()`):**371**(api 层 40 / 角色分包 52 / DOM 层 279)
 - 文档化用例(设计层 TC-xxxx):**71**
 - 已完成(自动化且未被跳过):**348/371 = 93%**
-- 已实跑通过:接口冒烟 BIZ_OK **245** 端点 + API e2e 末次 **0** 通过 / 0 fixme
+- 已实跑通过:接口冒烟 BIZ_OK **247** 端点 + API e2e 末次 **0** 通过 / 0 fixme
 
 ### 未完成 / 未开始的原因(共 4 类)
 1. [279 项] DOM 型 E2E 无法执行(CFC-ENV-001):279 个用例已编写,但远程 cfc.iwintrue.com 为纯 Spring Boot API 后端、未部署 uni-app H5 / cfc-web 前端,浏览器型用例无法运行,未开始执行。

+ 31 - 0
interface_test_bodies.tsv

@@ -0,0 +1,31 @@
+method	path	json
+POST	/api/admin/health-plan/delete	{"planId":1}
+POST	/api/admin/indicator/backfill	{"subjectId":1,"reportId":1,"limit":1,"dryRun":"x","fixReportDates":"x"}
+POST	/api/auth/check-phone	{"phone":"13800138000"}
+POST	/api/auth/phone-login	{"phone":"13800138000","code":"000000"}
+POST	/api/auth/register-with-invite	{"inviteCode":"000000"}
+POST	/api/auth/send-code	{"phone":"13800138000"}
+POST	/api/auth/set-password	{"password":"Test@123"}
+POST	/api/auth/switch-role	{"role":"parent"}
+POST	/api/auth/verify	{"password":"Test@123"}
+POST	/api/auth/wechat-login	{"code":"000000"}
+POST	/api/auth/wechat-phone-login	{"code":"000000"}
+POST	/api/family/invite/approve-request	{"requestId":1001,"comment":""}
+POST	/api/family/invite/bind-inviter	{"familyInviteCode":"fam_abc123"}
+POST	/api/family/invite/reject-request	{"requestId":1001,"comment":"不符合条件"}
+POST	/api/family/invite/validate	{"token":"abc123def456"}
+POST	/api/family/invite/validate-family-code	{"inviteCode":"000000"}
+POST	/api/family/member/kick	{"memberId":12345}
+POST	/api/family/member/recycle	{"memberId":12345,"targetFamilyId":67890}
+POST	/api/health/plan/delete	{"planId":1}
+POST	/api/health/report/indicators	{"reportId":1,"withPrevious":"x"}
+POST	/api/indicator/change-ranking	{"worsened":"x"}
+POST	/api/indicator/favorite/list	{"memberId":1,"sourceType":"x"}
+POST	/api/indicator/favorite/remove	{"memberId":1,"sourceType":"x","definitionId":1}
+POST	/api/indicator/favorite/toggle	{"memberId":1,"sourceType":"x","definitionId":1}
+POST	/api/indicator/periods	{"subjectId":1,"sourceType":"x"}
+POST	/api/indicator/trend	{"month":"x","sourceKind":"x","all":"x"}
+POST	/api/user/address/delete	{"id":1}
+POST	/api/user/address/get	{"id":1}
+POST	/api/user/address/setDefault	{"id":1}
+POST	/api/user/mascot/set	{"mascotId":1}

+ 33 - 2
ops_interface_smoke.py

@@ -1172,6 +1172,7 @@ def main():
     ap.add_argument('--report', default='', help='结果报告输出文件')
     ap.add_argument('--extra', default='', help='额外用例文件 TSV(page fn method path auth param),追加到 CASES,用于新需求/变更需求补全测试')
     ap.add_argument('--only-extra', action='store_true', help='仅运行 --extra 用例(跳过内置全量用例),用于聚焦业务级重测')
+    ap.add_argument('--body-map', default='', help='真实请求体映射 TSV(method<TAB>path<TAB>json),命中则覆盖用例默认空体,用于破除空体冒烟天花板')
     args = ap.parse_args()
 
     # 加载额外用例(新需求/变更需求补全):追加到 CASES 后再跑
@@ -1196,6 +1197,29 @@ def main():
         CASES[:] = extra_cases
         print(f'[only-extra] 仅运行 {len(CASES)} 条额外用例')
 
+    # 加载真实请求体映射(用于破除空体冒烟的天花板)
+    body_map = None
+    if args.body_map and os.path.exists(args.body_map):
+        body_map = {}
+        _bm = 0
+        with open(args.body_map, 'r', encoding='utf-8') as _bf:
+            for _line in _bf:
+                _line = _line.rstrip('\n')
+                if not _line or _line.startswith('method'):
+                    continue
+                _p = _line.split('\t')
+                if len(_p) < 3:
+                    continue
+                _m, _pt, _j = _p[0], _p[1], '\t'.join(_p[2:])
+                try:
+                    json.loads(_j)  # 校验合法 JSON,避免注入坏体
+                except Exception:
+                    print(f'[body-map] 跳过非法 JSON: {_m} {_pt}')
+                    continue
+                body_map[(_m.upper(), _pt)] = _j.encode('utf-8')
+                _bm += 1
+        print(f'已加载请求体映射: {_bm} 条(来自 {args.body_map})')
+
     tokens = {'parent': None, 'teacher': None}
     token = os.getenv('CFC_TOKEN')
     if args.auto_login and not token:
@@ -1207,7 +1231,8 @@ def main():
     stats = {}
     rows = []
     pages = set()
-    for page, method, path, auth, param, fn in CASES:
+    for case in CASES:
+        page, method, path, auth, param, fn, *rest = case
         if args.only_page and args.only_page not in page:
             continue
         if not token and auth:
@@ -1215,7 +1240,13 @@ def main():
         real = path.replace(':var', args.param_val)
         while '//' in real:
             real = real.replace('//', '/')
-        r = _call(args.base, method, real, token=token, timeout=args.timeout, host=args.host)
+        # 允许 --body-map 覆盖真实请求体(破除空体冒烟天花板)
+        body = rest[0] if rest else b'{}'
+        if body_map is not None:
+            _m = body_map.get((method, real)) or body_map.get((method, path))
+            if _m is not None:
+                body = _m
+        r = _call(args.base, method, real, token=token, timeout=args.timeout, host=args.host, body=body)
         http_code, biz, cat = _classify(r)
         stats[cat] = stats.get(cat, 0) + 1
         pages.add(page)