|
@@ -131,9 +131,9 @@ CASES = [
|
|
|
('__tests__/api/api.test', 'POST', '/api/tasks/today-parent', True, False, 'getTodayParentTasks'),
|
|
('__tests__/api/api.test', 'POST', '/api/tasks/today-parent', True, False, 'getTodayParentTasks'),
|
|
|
('__tests__/api/api.test', 'POST', '/api/user/info', True, False, 'getUserInfo'),
|
|
('__tests__/api/api.test', 'POST', '/api/user/info', True, False, 'getUserInfo'),
|
|
|
('__tests__/api/api.test', 'POST', '/api/user/join-family', True, False, 'joinFamily'),
|
|
('__tests__/api/api.test', 'POST', '/api/user/join-family', True, False, 'joinFamily'),
|
|
|
- ('__tests__/api/api.test', 'POST', '/api/user/switch-back-to-parent', True, False, 'switchBackToParent'),
|
|
|
|
|
- ('__tests__/api/api.test', 'POST', '/api/user/switch-mode', True, False, 'switchMode'),
|
|
|
|
|
- ('__tests__/api/api.test', 'POST', '/api/user/switch-to-child', True, False, 'switchToChild'),
|
|
|
|
|
|
|
+ ('__tests__/api/api.test', 'POST', '/api/family/user/switch-back-to-parent', True, False, 'switchBackToParent'),
|
|
|
|
|
+ ('__tests__/api/api.test', 'POST', '/api/family/user/switch-mode', True, False, 'switchMode'),
|
|
|
|
|
+ ('__tests__/api/api.test', 'POST', '/api/family/user/switch-to-child', True, False, 'switchToChild'),
|
|
|
('__tests__/api/api.test', 'POST', '/api/user/update', True, False, 'updateUserInfo'),
|
|
('__tests__/api/api.test', 'POST', '/api/user/update', True, False, 'updateUserInfo'),
|
|
|
('__tests__/api/api.test', 'POST', '/api/vendor/apply', True, False, 'vendorApply'),
|
|
('__tests__/api/api.test', 'POST', '/api/vendor/apply', True, False, 'vendorApply'),
|
|
|
('__tests__/api/api.test', 'POST', '/api/vendor/info', False, False, 'vendorInfo'),
|
|
('__tests__/api/api.test', 'POST', '/api/vendor/info', False, False, 'vendorInfo'),
|
|
@@ -146,7 +146,7 @@ CASES = [
|
|
|
('__tests__/api/api.test', 'POST', '/api/wishes/list', True, False, 'getWishes'),
|
|
('__tests__/api/api.test', 'POST', '/api/wishes/list', True, False, 'getWishes'),
|
|
|
('__tests__/api/api.test', 'POST', '/api/wishes/pending', True, False, 'getPendingWishes'),
|
|
('__tests__/api/api.test', 'POST', '/api/wishes/pending', True, False, 'getPendingWishes'),
|
|
|
('__tests__/api/api.test', 'POST', '/api/withdrawal/apply', True, False, 'applyWithdrawal'),
|
|
('__tests__/api/api.test', 'POST', '/api/withdrawal/apply', True, False, 'applyWithdrawal'),
|
|
|
- ('__tests__/store/store.test', 'POST', '/api/user/switch-role', True, False, 'switchRole'),
|
|
|
|
|
|
|
+ ('__tests__/store/store.test', 'POST', '/api/auth/switch-role', True, False, 'switchRole'),
|
|
|
('components/DimensionActivities', 'POST', '/api/activity/list', False, False, 'getActivityList'),
|
|
('components/DimensionActivities', 'POST', '/api/activity/list', False, False, 'getActivityList'),
|
|
|
('components/DimensionActivities', 'POST', '/api/membership/my', True, False, 'getMyMembership'),
|
|
('components/DimensionActivities', 'POST', '/api/membership/my', True, False, 'getMyMembership'),
|
|
|
('components/DimensionProducts', 'POST', '/api/recommend/dimension-products', True, False, 'getDimensionProducts'),
|
|
('components/DimensionProducts', 'POST', '/api/recommend/dimension-products', True, False, 'getDimensionProducts'),
|
|
@@ -292,10 +292,10 @@ CASES = [
|
|
|
('pages/child/rewards', 'POST', '/api/auth/verify-password', False, False, 'verifyPassword'),
|
|
('pages/child/rewards', 'POST', '/api/auth/verify-password', False, False, 'verifyPassword'),
|
|
|
('pages/child/rewards', 'POST', '/api/points/balance', True, False, 'getPointsBalance'),
|
|
('pages/child/rewards', 'POST', '/api/points/balance', True, False, 'getPointsBalance'),
|
|
|
('pages/child/rewards', 'POST', '/api/rewards/wishlist', True, False, 'getWishlist'),
|
|
('pages/child/rewards', 'POST', '/api/rewards/wishlist', True, False, 'getWishlist'),
|
|
|
- ('pages/child/rewards', 'POST', '/api/user/switch-back-to-parent', True, False, 'switchBackToParent'),
|
|
|
|
|
|
|
+ ('pages/child/rewards', 'POST', '/api/family/user/switch-back-to-parent', True, False, 'switchBackToParent'),
|
|
|
('pages/child/tasks', 'POST', '/api/auth/verify-password', False, False, 'verifyPassword'),
|
|
('pages/child/tasks', 'POST', '/api/auth/verify-password', False, False, 'verifyPassword'),
|
|
|
('pages/child/tasks', 'POST', '/api/tasks/today', True, False, 'getTodayTasks'),
|
|
('pages/child/tasks', 'POST', '/api/tasks/today', True, False, 'getTodayTasks'),
|
|
|
- ('pages/child/tasks', 'POST', '/api/user/switch-back-to-parent', True, False, 'switchBackToParent'),
|
|
|
|
|
|
|
+ ('pages/child/tasks', 'POST', '/api/family/user/switch-back-to-parent', True, False, 'switchBackToParent'),
|
|
|
('pages/cognitive-hall', 'POST', '/api/cognitive/child', True, False, 'getCognitiveChildScores'),
|
|
('pages/cognitive-hall', 'POST', '/api/cognitive/child', True, False, 'getCognitiveChildScores'),
|
|
|
('pages/cognitive-hall', 'POST', '/api/family/member/list', True, False, 'getFamilyMemberList'),
|
|
('pages/cognitive-hall', 'POST', '/api/family/member/list', True, False, 'getFamilyMemberList'),
|
|
|
('pages/cognitive-test/language-test', 'POST', '/api/cognitive/self-test', True, False, 'submitCognitiveSelfTest'),
|
|
('pages/cognitive-test/language-test', 'POST', '/api/cognitive/self-test', True, False, 'submitCognitiveSelfTest'),
|
|
@@ -554,7 +554,7 @@ CASES = [
|
|
|
('pages/home-pages/child-index', 'POST', '/api/guide/popup/list', True, False, 'getGuidePopupList'),
|
|
('pages/home-pages/child-index', 'POST', '/api/guide/popup/list', True, False, 'getGuidePopupList'),
|
|
|
('pages/home-pages/child-index', 'POST', '/api/tasks/today', True, False, 'getTodayTasks'),
|
|
('pages/home-pages/child-index', 'POST', '/api/tasks/today', True, False, 'getTodayTasks'),
|
|
|
('pages/home-pages/child-index', 'POST', '/api/unlock/status', True, False, 'getUnlockStatus'),
|
|
('pages/home-pages/child-index', 'POST', '/api/unlock/status', True, False, 'getUnlockStatus'),
|
|
|
- ('pages/home-pages/child-index', 'POST', '/api/user/switch-mode', True, False, 'switchMode'),
|
|
|
|
|
|
|
+ ('pages/home-pages/child-index', 'POST', '/api/family/user/switch-mode', True, False, 'switchMode'),
|
|
|
('pages/home-pages/components/AdBanner', 'POST', '/api/home-banner/list', True, False, 'getHomeBanners'),
|
|
('pages/home-pages/components/AdBanner', 'POST', '/api/home-banner/list', True, False, 'getHomeBanners'),
|
|
|
('pages/home-pages/components/ContactImport', 'POST', '/api/contact/import-phone', True, False, 'importPhoneContact'),
|
|
('pages/home-pages/components/ContactImport', 'POST', '/api/contact/import-phone', True, False, 'importPhoneContact'),
|
|
|
('pages/home-pages/components/DailySummary', 'POST', '/api/feedback/read', False, False, 'markFeedbackRead'),
|
|
('pages/home-pages/components/DailySummary', 'POST', '/api/feedback/read', False, False, 'markFeedbackRead'),
|
|
@@ -890,7 +890,7 @@ CASES = [
|
|
|
('pages/teacher/review-workbench', 'POST', '/api/guide/families/my-families', True, False, 'getGuideMyFamilies'),
|
|
('pages/teacher/review-workbench', 'POST', '/api/guide/families/my-families', True, False, 'getGuideMyFamilies'),
|
|
|
('pages/teacher/service-tasks', 'POST', '/api/tasks//:var/complete', True, True, 'completeTask'),
|
|
('pages/teacher/service-tasks', 'POST', '/api/tasks//:var/complete', True, True, 'completeTask'),
|
|
|
('pages/teacher/teacher-profile', 'POST', '/api/guide/parent-invite/generate', True, False, 'generateParentInvite'),
|
|
('pages/teacher/teacher-profile', 'POST', '/api/guide/parent-invite/generate', True, False, 'generateParentInvite'),
|
|
|
- ('pages/teacher/teacher-profile', 'POST', '/api/user/switch-role', True, False, 'switchRole'),
|
|
|
|
|
|
|
+ ('pages/teacher/teacher-profile', 'POST', '/api/auth/switch-role', True, False, 'switchRole'),
|
|
|
('pages/teacher/team', 'POST', '/api/guide/invite-code/generate', False, False, 'generateInviteCode'),
|
|
('pages/teacher/team', 'POST', '/api/guide/invite-code/generate', False, False, 'generateInviteCode'),
|
|
|
('pages/teacher/team', 'POST', '/api/guide/team/invite-code/generate', True, False, 'generateTeamInviteCode'),
|
|
('pages/teacher/team', 'POST', '/api/guide/team/invite-code/generate', True, False, 'generateTeamInviteCode'),
|
|
|
('pages/teacher/team', 'POST', '/api/guide/team/members', True, False, 'getTeamMembers'),
|
|
('pages/teacher/team', 'POST', '/api/guide/team/members', True, False, 'getTeamMembers'),
|
|
@@ -981,9 +981,9 @@ CASES = [
|
|
|
('store/health-score', 'POST', '/api/health/score/info', True, False, 'getHealthScoreInfo'),
|
|
('store/health-score', 'POST', '/api/health/score/info', True, False, 'getHealthScoreInfo'),
|
|
|
('store/health-score', 'POST', '/api/health/timeline/list', True, False, 'getHealthTimeline'),
|
|
('store/health-score', 'POST', '/api/health/timeline/list', True, False, 'getHealthTimeline'),
|
|
|
('store/index', 'POST', '/api/family/user/switch-back-verify', True, False, 'switchBackVerify'),
|
|
('store/index', 'POST', '/api/family/user/switch-back-verify', True, False, 'switchBackVerify'),
|
|
|
- ('store/index', 'POST', '/api/user/switch-back-to-parent', True, False, 'switchBackToParent'),
|
|
|
|
|
- ('store/index', 'POST', '/api/user/switch-role', True, False, 'switchRole'),
|
|
|
|
|
- ('store/index', 'POST', '/api/user/switch-to-child', True, False, 'switchToChild'),
|
|
|
|
|
|
|
+ ('store/index', 'POST', '/api/family/user/switch-back-to-parent', True, False, 'switchBackToParent'),
|
|
|
|
|
+ ('store/index', 'POST', '/api/auth/switch-role', True, False, 'switchRole'),
|
|
|
|
|
+ ('store/index', 'POST', '/api/family/user/switch-to-child', True, False, 'switchToChild'),
|
|
|
('store/modules/tianpan', 'POST', '/api/tianpan/annual-energy', False, False, 'tianpanAnnualEnergy'),
|
|
('store/modules/tianpan', 'POST', '/api/tianpan/annual-energy', False, False, 'tianpanAnnualEnergy'),
|
|
|
('store/modules/tianpan', 'POST', '/api/tianpan/daily-fortune', False, False, 'tianpanDailyFortune'),
|
|
('store/modules/tianpan', 'POST', '/api/tianpan/daily-fortune', False, False, 'tianpanDailyFortune'),
|
|
|
('store/modules/tianpan', 'POST', '/api/tianpan/dashboard', False, False, 'tianpanDashboard'),
|
|
('store/modules/tianpan', 'POST', '/api/tianpan/dashboard', False, False, 'tianpanDashboard'),
|
|
@@ -1087,21 +1087,74 @@ def auto_login(base, host=None, timeout=10):
|
|
|
return None
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+def direct_register(base, host=None, timeout=10, phone=None, role=None):
|
|
|
|
|
+ """测试首选登录:跳过验证码直接注册/登录(AuthController.direct-register)。
|
|
|
|
|
+ 返回 (token, info);失败返回 (None, None)。phone 不传则按时间生成唯一号避免『已注册』。"""
|
|
|
|
|
+ if not phone:
|
|
|
|
|
+ phone = '138' + str(int(time.time() * 1000) % 10 ** 8).zfill(8)
|
|
|
|
|
+ body = json.dumps({'phone': phone, 'nickname': 'smoke-bot',
|
|
|
|
|
+ 'role': role or 'parent'}).encode('utf-8')
|
|
|
|
|
+ r = _call(base, 'POST', '/api/auth/direct-register', timeout=timeout, host=host, body=body)
|
|
|
|
|
+ try:
|
|
|
|
|
+ d = r.json()
|
|
|
|
|
+ tok = (d.get('data') or {}).get('token')
|
|
|
|
|
+ if tok:
|
|
|
|
|
+ info = d.get('data') or {}
|
|
|
|
|
+ print(f'[登录] direct-register 成功 phone={phone} userId={info.get("userId")} '
|
|
|
|
|
+ f'role={info.get("role")}')
|
|
|
|
|
+ return tok, info
|
|
|
|
|
+ except Exception:
|
|
|
|
|
+ pass
|
|
|
|
|
+ print('[登录] direct-register 失败: ' + str(getattr(r, 'text', ''))[:120])
|
|
|
|
|
+ return None, None
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def acquire_tokens(base, host=None, timeout=10, want_teacher=False):
|
|
|
|
|
+ """多角色 token 前置:返回 {'parent': tok, 'teacher': tok}。
|
|
|
|
|
+ 父 token 优先 wechat-login(测试模式空 body),失败回退 direct-register;
|
|
|
|
|
+ 师 token 仅当 want_teacher=True 时用 direct-register(role=teacher) 获取
|
|
|
|
|
+ (避免定时任务每轮都新建教师账号)。任一获取失败则为 None(不影响其它用例)。"""
|
|
|
|
|
+ tokens = {'parent': None, 'teacher': None}
|
|
|
|
|
+ # 父 token
|
|
|
|
|
+ tok = auto_login(base, host=host, timeout=timeout)
|
|
|
|
|
+ if not tok:
|
|
|
|
|
+ tok, _ = direct_register(base, host=host, timeout=timeout, role='parent')
|
|
|
|
|
+ tokens['parent'] = tok
|
|
|
|
|
+ # 师 token(按需)
|
|
|
|
|
+ if want_teacher:
|
|
|
|
|
+ tok, _ = direct_register(base, host=host, timeout=timeout, role='teacher')
|
|
|
|
|
+ tokens['teacher'] = tok
|
|
|
|
|
+ if not tokens['parent'] and not tokens['teacher']:
|
|
|
|
|
+ print('[登录] 警告:父/师 token 均未取得,登录态接口将被跳过')
|
|
|
|
|
+ return tokens
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
def _classify(r):
|
|
def _classify(r):
|
|
|
- """返回 (http_code, biz_code, 类别)"""
|
|
|
|
|
|
|
+ """返回 (http_code, biz_code, 类别)
|
|
|
|
|
+
|
|
|
|
|
+ 契约基线(见 INTERFACE_CONTRACT_REVIEW.md):
|
|
|
|
|
+ - 成功 = Result.code == 200;其余为非 0 失败码(含 401/403/410/5001/4001 等)
|
|
|
|
|
+ - 不带 token 访问非白名单接口:JwtInterceptor 注入匿名用户放行,
|
|
|
|
|
+ HTTP 仍是 200,但业务 code 非 200(多为 500)——归为 BIZ_FAIL(伪 200)
|
|
|
|
|
+ - 真正未登录/ token 非法或命中强制登录路径:HTTP 401 —— 归为 AUTH_FAIL
|
|
|
|
|
+ """
|
|
|
if r.status_code == -1:
|
|
if r.status_code == -1:
|
|
|
return -1, None, 'CONN_ERR'
|
|
return -1, None, 'CONN_ERR'
|
|
|
try:
|
|
try:
|
|
|
d = r.json()
|
|
d = r.json()
|
|
|
|
|
+ biz = d.get('code') if isinstance(d, dict) else None
|
|
|
|
|
+ msg = (d.get('message') if isinstance(d, dict) else None)
|
|
|
except Exception:
|
|
except Exception:
|
|
|
return r.status_code, None, 'NON_JSON'
|
|
return r.status_code, None, 'NON_JSON'
|
|
|
- biz = d.get('code') if isinstance(d, dict) else None
|
|
|
|
|
if r.status_code == 404:
|
|
if r.status_code == 404:
|
|
|
return r.status_code, biz, 'NOT_FOUND'
|
|
return r.status_code, biz, 'NOT_FOUND'
|
|
|
|
|
+ if r.status_code == 401:
|
|
|
|
|
+ return r.status_code, biz, 'AUTH_FAIL' # 真未登录 / token 非法 / 强制登录路径
|
|
|
if r.status_code >= 500:
|
|
if r.status_code >= 500:
|
|
|
return r.status_code, biz, 'SERVER_ERR'
|
|
return r.status_code, biz, 'SERVER_ERR'
|
|
|
if biz == 200:
|
|
if biz == 200:
|
|
|
return r.status_code, biz, 'BIZ_OK'
|
|
return r.status_code, biz, 'BIZ_OK'
|
|
|
|
|
+ # 匿名放行或业务自定义失败码(410/403/5001/4001/500 等)统一归 BIZ_FAIL
|
|
|
return r.status_code, biz, 'BIZ_FAIL'
|
|
return r.status_code, biz, 'BIZ_FAIL'
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1110,16 +1163,19 @@ def main():
|
|
|
ap.add_argument('--base', default=os.getenv('CFC_BASE', 'http://192.168.16.251'))
|
|
ap.add_argument('--base', default=os.getenv('CFC_BASE', 'http://192.168.16.251'))
|
|
|
ap.add_argument('--host', default=os.getenv('CFC_HOST', 'cfc.iwintrue.com'),
|
|
ap.add_argument('--host', default=os.getenv('CFC_HOST', 'cfc.iwintrue.com'),
|
|
|
help='Host 头(域名 DNS 不通时用 IP + Host 头访问)')
|
|
help='Host 头(域名 DNS 不通时用 IP + Host 头访问)')
|
|
|
- ap.add_argument('--auto-login', action='store_true', help='自动调 wechat-login 换 token')
|
|
|
|
|
|
|
+ ap.add_argument('--auto-login', action='store_true', help='自动登录换 token(优先 wechat-login,回退 direct-register)')
|
|
|
ap.add_argument('--authed', action='store_true', help='同时跑登录态接口')
|
|
ap.add_argument('--authed', action='store_true', help='同时跑登录态接口')
|
|
|
|
|
+ ap.add_argument('--multi-role', action='store_true', help='额外获取 teacher token 并跑多角色验证用例')
|
|
|
ap.add_argument('--param-val', default='1', help='路径参数 :var 的占位值')
|
|
ap.add_argument('--param-val', default='1', help='路径参数 :var 的占位值')
|
|
|
ap.add_argument('--timeout', type=int, default=10)
|
|
ap.add_argument('--timeout', type=int, default=10)
|
|
|
ap.add_argument('--only-page', default='', help='只跑指定页面路由(含匹配即可)')
|
|
ap.add_argument('--only-page', default='', help='只跑指定页面路由(含匹配即可)')
|
|
|
ap.add_argument('--report', default='', help='结果报告输出文件')
|
|
ap.add_argument('--report', default='', help='结果报告输出文件')
|
|
|
ap.add_argument('--extra', default='', help='额外用例文件 TSV(page fn method path auth param),追加到 CASES,用于新需求/变更需求补全测试')
|
|
ap.add_argument('--extra', default='', help='额外用例文件 TSV(page fn method path auth param),追加到 CASES,用于新需求/变更需求补全测试')
|
|
|
|
|
+ ap.add_argument('--only-extra', action='store_true', help='仅运行 --extra 用例(跳过内置全量用例),用于聚焦业务级重测')
|
|
|
args = ap.parse_args()
|
|
args = ap.parse_args()
|
|
|
|
|
|
|
|
# 加载额外用例(新需求/变更需求补全):追加到 CASES 后再跑
|
|
# 加载额外用例(新需求/变更需求补全):追加到 CASES 后再跑
|
|
|
|
|
+ extra_cases = []
|
|
|
if args.extra and os.path.exists(args.extra):
|
|
if args.extra and os.path.exists(args.extra):
|
|
|
_added = 0
|
|
_added = 0
|
|
|
with open(args.extra, 'r', encoding='utf-8') as _ef:
|
|
with open(args.extra, 'r', encoding='utf-8') as _ef:
|
|
@@ -1131,13 +1187,21 @@ def main():
|
|
|
if len(_p) < 6:
|
|
if len(_p) < 6:
|
|
|
continue
|
|
continue
|
|
|
_page, _fn, _method, _path, _auth, _param = _p[:6]
|
|
_page, _fn, _method, _path, _auth, _param = _p[:6]
|
|
|
- CASES.append((_page, _method, _path, _auth.lower() == 'true', _param.lower() == 'true', _fn))
|
|
|
|
|
|
|
+ _c = (_page, _method, _path, _auth.lower() == 'true', _param.lower() == 'true', _fn)
|
|
|
|
|
+ extra_cases.append(_c)
|
|
|
|
|
+ CASES.append(_c)
|
|
|
_added += 1
|
|
_added += 1
|
|
|
print(f'已追加额外用例: {_added} 条(来自 {args.extra})')
|
|
print(f'已追加额外用例: {_added} 条(来自 {args.extra})')
|
|
|
|
|
+ if args.only_extra and extra_cases:
|
|
|
|
|
+ CASES[:] = extra_cases
|
|
|
|
|
+ print(f'[only-extra] 仅运行 {len(CASES)} 条额外用例')
|
|
|
|
|
|
|
|
|
|
+ tokens = {'parent': None, 'teacher': None}
|
|
|
token = os.getenv('CFC_TOKEN')
|
|
token = os.getenv('CFC_TOKEN')
|
|
|
if args.auto_login and not token:
|
|
if args.auto_login and not token:
|
|
|
- token = auto_login(args.base, host=args.host, timeout=args.timeout)
|
|
|
|
|
|
|
+ tokens = acquire_tokens(args.base, host=args.host, timeout=args.timeout,
|
|
|
|
|
+ want_teacher=args.multi_role)
|
|
|
|
|
+ token = tokens['parent']
|
|
|
|
|
|
|
|
print(f'目标: {args.base} Host: {args.host} token: {"有" if token else "无"}')
|
|
print(f'目标: {args.base} Host: {args.host} token: {"有" if token else "无"}')
|
|
|
stats = {}
|
|
stats = {}
|
|
@@ -1157,10 +1221,51 @@ def main():
|
|
|
pages.add(page)
|
|
pages.add(page)
|
|
|
rows.append((page, fn, method, real, http_code, biz, cat))
|
|
rows.append((page, fn, method, real, http_code, biz, cat))
|
|
|
|
|
|
|
|
|
|
+ # ---- 多角色验证(teacher token 前置)----
|
|
|
|
|
+ if args.multi_role and tokens.get('teacher'):
|
|
|
|
|
+ print('\n=== 多角色验证(teacher token)===')
|
|
|
|
|
+
|
|
|
|
|
+ def _msg(rr):
|
|
|
|
|
+ try:
|
|
|
|
|
+ j = rr.json()
|
|
|
|
|
+ return (j.get('message') if isinstance(j, dict) else '') or ''
|
|
|
|
|
+ except Exception:
|
|
|
|
|
+ return ''
|
|
|
|
|
+
|
|
|
|
|
+ def _run(page, fn, method, path, body, tok):
|
|
|
|
|
+ rr = _call(args.base, method, path, token=tok, timeout=args.timeout,
|
|
|
|
|
+ host=args.host, body=body)
|
|
|
|
|
+ hc, bz, ct = _classify(rr)
|
|
|
|
|
+ stats[ct] = stats.get(ct, 0) + 1
|
|
|
|
|
+ pages.add(page)
|
|
|
|
|
+ rows.append((page, fn, method, path, hc, bz, ct))
|
|
|
|
|
+ return rr, hc, bz, ct
|
|
|
|
|
+
|
|
|
|
|
+ # 1) teacher token 本身有效(verify 应 BIZ_OK)
|
|
|
|
|
+ rr, hc, bz, ct = _run('__multi_role__', 'teacherVerify', 'POST',
|
|
|
|
|
+ '/api/auth/verify', b'{}', tokens['teacher'])
|
|
|
|
|
+ print(f' [teacher] /api/auth/verify -> {ct} (biz={bz}) 期望 BIZ_OK')
|
|
|
|
|
+
|
|
|
|
|
+ # 2) 同一 teacher-only 接口:parent 应被拒、teacher 应进入业务校验(均不应 404/连接错)
|
|
|
|
|
+ body = b'{"inviteCode":"__smoke_dummy__"}'
|
|
|
|
|
+ rr_p, hcp, bzp, ctp = _run('__multi_role__', 'guideBindAsParent', 'POST',
|
|
|
|
|
+ '/api/family/guide-bind', body, tokens['parent'])
|
|
|
|
|
+ rr_t, hct, bzt, ctt = _run('__multi_role__', 'guideBindAsTeacher', 'POST',
|
|
|
|
|
+ '/api/family/guide-bind', body, tokens['teacher'])
|
|
|
|
|
+ mp, mt = _msg(rr_p), _msg(rr_t)
|
|
|
|
|
+ print(f' [parent ] /api/family/guide-bind -> {ctp} msg="{mp[:36]}"')
|
|
|
|
|
+ print(f' [teacher] /api/family/guide-bind -> {ctt} msg="{mt[:36]}"')
|
|
|
|
|
+
|
|
|
|
|
+ # 3) 权限接口:非 admin 调 rebuild 应 "权限不足"
|
|
|
|
|
+ rr, hc, bz, ct = _run('__multi_role__', 'indicatorsRebuildParent', 'POST',
|
|
|
|
|
+ '/api/health-report/indicators/rebuild',
|
|
|
|
|
+ b'{"memberId":1}', tokens['parent'])
|
|
|
|
|
+ print(f' [parent] /api/health-report/indicators/rebuild -> {ct} (biz={bz}) msg="{_msg(rr)[:36]}"')
|
|
|
|
|
+
|
|
|
total = len(rows)
|
|
total = len(rows)
|
|
|
print(f'\n=== 汇总 ===')
|
|
print(f'\n=== 汇总 ===')
|
|
|
print(f'页面覆盖: {len(pages)} 已请求接口: {total}')
|
|
print(f'页面覆盖: {len(pages)} 已请求接口: {total}')
|
|
|
- for k in ('BIZ_OK', 'BIZ_FAIL', 'NOT_FOUND', 'SERVER_ERR', 'NON_JSON', 'CONN_ERR'):
|
|
|
|
|
|
|
+ for k in ('BIZ_OK', 'BIZ_FAIL', 'AUTH_FAIL', 'NOT_FOUND', 'SERVER_ERR', 'NON_JSON', 'CONN_ERR'):
|
|
|
if k in stats:
|
|
if k in stats:
|
|
|
print(f' {k:<10}: {stats[k]}')
|
|
print(f' {k:<10}: {stats[k]}')
|
|
|
ok = stats.get('BIZ_OK', 0)
|
|
ok = stats.get('BIZ_OK', 0)
|