import json from pathlib import Path PROJECT_ROOT = Path(__file__).resolve().parents[1] p = PROJECT_ROOT / 'docs' / 'openapi-rc.json' d = json.load(open(p, encoding='utf-8')) print('paths:', len(d['paths'])) assert len(d['paths']) == 159, f"Expected 159 paths, got {len(d['paths'])}" paths = d['paths'] required = [ '/review-weight-templates', '/profile/me/review-weights', '/profile/me/privacy', '/jobs/{jobId}/personalized-review-score', '/company/candidates', '/company/candidates/{applicationId}', '/profile/me/candidate-scoring', '/profile/me/candidate-scoring/dimensions/{dimension}', '/profile/me/candidate-scoring/trend', '/company/candidates/{applicationId}/scoring', '/profile/me/candidate-scoring/appeals', '/profile/me/candidate-scoring/appeals/{appealId}', '/profile/me/candidate-scoring/appeals/{appealId}/cancel', '/admin/candidate-scoring/appeals', '/admin/candidate-scoring/appeals/{appealId}/decision', '/admin/candidate-scoring/appeals/{appealId}/rollback', '/admin/candidate-scoring/appeals/{appealId}/history', '/admin/candidate-scoring/snapshots/{candidateUserId}/{dimension}/{period}/history', # 企业评分运营看板(7 个接口) '/company/scoring/dashboard/overview', '/company/scoring/dashboard/job-rankings', '/company/scoring/dashboard/job-rankings/{jobId}', '/company/scoring/dashboard/candidate-rankings', '/company/scoring/dashboard/candidate-rankings/{applicationId}', '/company/scoring/dashboard/trend', '/admin/scoring/dashboard/overview', # Offer 进阶(5 个 path / 6 个 operation) '/offers/{id}/counter-offers', '/offers/{id}/counter-offers/{coId}/accept', '/offers/{id}/counter-offers/{coId}/reject', '/offers/compare', '/offers/statistics', # 账号注销(2 个 path / 3 个 operation) '/profile/me/deletion', '/profile/me/deletion/cancel', # 个人数据导出(3 个 path / 4 个 operation;详情已覆盖状态查询,无需 /status) '/profile/me/data-export-tasks', '/profile/me/data-export-tasks/{taskId}', '/profile/me/data-export-tasks/{taskId}/download', # 结构化面试评分(3 个 path / 4 个 operation) '/interviews/{interviewId}/score', '/interviews/{interviewId}/score/submit', '/interviews/{interviewId}/score/history', # 同岗位竞争力排名(1 个 path / 1 个 operation) '/jobs/{jobId}/competition-ranking', # V34 统一求职进度聚合(1 个 path / 1 个 operation) '/profile/me/career-progress', # V33 举报与屏蔽(8 个 path / 9 个 operation) '/reports', '/reports/me', '/reports/{id}', '/blocks/{blockedUserId}', '/blocks/me', '/admin/reports', '/admin/reports/{id}', '/admin/reports/{id}/decision', # V34/V35 RIASEC 职业兴趣测评(8 个 path / 8 个 operation) '/assessments/types', '/assessments/{type}/questions', '/assessments/attempts', '/assessments/attempts/{attemptId}', '/assessments/attempts/{attemptId}/answers', '/assessments/attempts/{attemptId}/submit', '/assessments/attempts/{attemptId}/result', '/assessments/me', # V36 四维能力测评(7 个 path / 7 个 operation) '/abilities/types', '/abilities/attempts', '/abilities/attempts/{attemptId}', '/abilities/attempts/{attemptId}/answers', '/abilities/attempts/{attemptId}/submit', '/abilities/attempts/{attemptId}/result', '/abilities/me', # V37 风险概览与运营规则配置(7 个 path / 7 个 operation,仅 ADMIN) '/admin/scoring/risk/overview', '/admin/scoring/risk/trend', '/admin/scoring/risk/distribution', '/admin/scoring/risk/events', '/admin/scoring/risk/rules', '/admin/scoring/risk/rules/{ruleCode}', '/admin/scoring/risk/events/{eventId}/actions', ] for path in required: assert path in paths, f"Missing path: {path}" print(f" Found: {path}") assert 'get' in paths['/review-weight-templates'], "Missing GET /review-weight-templates" assert 'get' in paths['/profile/me/review-weights'], "Missing GET /profile/me/review-weights" assert 'put' in paths['/profile/me/review-weights'], "Missing PUT /profile/me/review-weights" assert 'get' in paths['/profile/me/privacy'], "Missing GET /profile/me/privacy" assert 'put' in paths['/profile/me/privacy'], "Missing PUT /profile/me/privacy" assert 'get' in paths['/jobs/{jobId}/personalized-review-score'], "Missing GET /jobs/{jobId}/personalized-review-score" assert 'get' in paths['/company/candidates'], "Missing GET /company/candidates" assert 'get' in paths['/company/candidates/{applicationId}'], "Missing GET /company/candidates/{applicationId}" assert 'get' in paths['/profile/me/candidate-scoring'], "Missing GET /profile/me/candidate-scoring" assert 'get' in paths['/profile/me/candidate-scoring/dimensions/{dimension}'], "Missing GET /profile/me/candidate-scoring/dimensions/{dimension}" assert 'get' in paths['/profile/me/candidate-scoring/trend'], "Missing GET /profile/me/candidate-scoring/trend" assert 'get' in paths['/company/candidates/{applicationId}/scoring'], "Missing GET /company/candidates/{applicationId}/scoring" # 评分治理(9 个接口落在 8 个路径:POST+GET 共享 /appeals) assert 'post' in paths['/profile/me/candidate-scoring/appeals'], "Missing POST /profile/me/candidate-scoring/appeals" assert 'get' in paths['/profile/me/candidate-scoring/appeals'], "Missing GET /profile/me/candidate-scoring/appeals" assert 'get' in paths['/profile/me/candidate-scoring/appeals/{appealId}'], "Missing GET .../appeals/{appealId}" assert 'post' in paths['/profile/me/candidate-scoring/appeals/{appealId}/cancel'], "Missing POST .../cancel" assert 'get' in paths['/admin/candidate-scoring/appeals'], "Missing GET /admin/candidate-scoring/appeals" assert 'post' in paths['/admin/candidate-scoring/appeals/{appealId}/decision'], "Missing POST .../decision" assert 'post' in paths['/admin/candidate-scoring/appeals/{appealId}/rollback'], "Missing POST .../rollback" assert 'get' in paths['/admin/candidate-scoring/appeals/{appealId}/history'], "Missing GET .../history" assert 'get' in paths['/admin/candidate-scoring/snapshots/{candidateUserId}/{dimension}/{period}/history'], "Missing GET snapshots history" # 企业评分运营看板(7 个 GET) assert 'get' in paths['/company/scoring/dashboard/overview'], "Missing GET /company/scoring/dashboard/overview" assert 'get' in paths['/company/scoring/dashboard/job-rankings'], "Missing GET /company/scoring/dashboard/job-rankings" assert 'get' in paths['/company/scoring/dashboard/job-rankings/{jobId}'], "Missing GET /company/scoring/dashboard/job-rankings/{jobId}" assert 'get' in paths['/company/scoring/dashboard/candidate-rankings'], "Missing GET /company/scoring/dashboard/candidate-rankings" assert 'get' in paths['/company/scoring/dashboard/candidate-rankings/{applicationId}'], "Missing GET /company/scoring/dashboard/candidate-rankings/{applicationId}" assert 'get' in paths['/company/scoring/dashboard/trend'], "Missing GET /company/scoring/dashboard/trend" assert 'get' in paths['/admin/scoring/dashboard/overview'], "Missing GET /admin/scoring/dashboard/overview" # Offer 进阶(6 个 operation / 5 个 path) assert 'post' in paths['/offers/{id}/counter-offers'], "Missing POST /offers/{id}/counter-offers" assert 'get' in paths['/offers/{id}/counter-offers'], "Missing GET /offers/{id}/counter-offers" assert 'patch' in paths['/offers/{id}/counter-offers/{coId}/accept'], "Missing PATCH .../accept" assert 'patch' in paths['/offers/{id}/counter-offers/{coId}/reject'], "Missing PATCH .../reject" assert 'get' in paths['/offers/compare'], "Missing GET /offers/compare" assert 'get' in paths['/offers/statistics'], "Missing GET /offers/statistics" # 账号注销(3 个 operation) assert 'get' in paths['/profile/me/deletion'], "Missing GET /profile/me/deletion" assert 'post' in paths['/profile/me/deletion'], "Missing POST /profile/me/deletion" assert 'post' in paths['/profile/me/deletion/cancel'], "Missing POST /profile/me/deletion/cancel" # 个人数据导出(4 个 operation / 3 个 path) assert 'post' in paths['/profile/me/data-export-tasks'], "Missing POST /profile/me/data-export-tasks" assert 'get' in paths['/profile/me/data-export-tasks'], "Missing GET /profile/me/data-export-tasks" assert 'get' in paths['/profile/me/data-export-tasks/{taskId}'], "Missing GET /profile/me/data-export-tasks/{taskId}" assert 'get' in paths['/profile/me/data-export-tasks/{taskId}/download'], "Missing GET .../download" print(" Personal data export endpoints present (4 operations / 3 paths)") # 同岗位竞争力排名(1 个 GET operation) assert 'get' in paths['/jobs/{jobId}/competition-ranking'], "Missing GET /jobs/{jobId}/competition-ranking" print(" Competition ranking endpoint present (1 GET operation)") # V34 统一求职进度聚合(1 个 GET operation) assert 'get' in paths['/profile/me/career-progress'], "Missing GET /profile/me/career-progress" print(" Career progress endpoint present (1 GET operation)") # V33 举报与屏蔽(8 个 path / 9 个 operation;PUT+DELETE 共享 /blocks/{blockedUserId}) assert 'post' in paths['/reports'], "Missing POST /reports" assert 'get' in paths['/reports/me'], "Missing GET /reports/me" assert 'get' in paths['/reports/{id}'], "Missing GET /reports/{id}" assert 'put' in paths['/blocks/{blockedUserId}'], "Missing PUT /blocks/{blockedUserId}" assert 'delete' in paths['/blocks/{blockedUserId}'], "Missing DELETE /blocks/{blockedUserId}" assert 'get' in paths['/blocks/me'], "Missing GET /blocks/me" assert 'get' in paths['/admin/reports'], "Missing GET /admin/reports" assert 'get' in paths['/admin/reports/{id}'], "Missing GET /admin/reports/{id}" assert 'post' in paths['/admin/reports/{id}/decision'], "Missing POST /admin/reports/{id}/decision" print(" V33 report/block endpoints present (9 operations / 8 paths)") # V34/V35 RIASEC 测评(8 个 path / 8 个 operation) assert 'get' in paths['/assessments/types'], "Missing GET /assessments/types" assert 'get' in paths['/assessments/{type}/questions'], "Missing GET /assessments/{type}/questions" assert 'post' in paths['/assessments/attempts'], "Missing POST /assessments/attempts" assert 'get' in paths['/assessments/attempts/{attemptId}'], "Missing GET /assessments/attempts/{attemptId}" assert 'patch' in paths['/assessments/attempts/{attemptId}/answers'], "Missing PATCH .../answers" assert 'post' in paths['/assessments/attempts/{attemptId}/submit'], "Missing POST .../submit" assert 'get' in paths['/assessments/attempts/{attemptId}/result'], "Missing GET .../result" assert 'get' in paths['/assessments/me'], "Missing GET /assessments/me" print(" V34/V35 RIASEC assessment endpoints present (8 operations / 8 paths)") # V36 四维能力测评(7 个 path / 7 个 operation) assert 'get' in paths['/abilities/types'], "Missing GET /abilities/types" assert 'post' in paths['/abilities/attempts'], "Missing POST /abilities/attempts" assert 'get' in paths['/abilities/attempts/{attemptId}'], "Missing GET /abilities/attempts/{attemptId}" assert 'patch' in paths['/abilities/attempts/{attemptId}/answers'], "Missing PATCH .../answers" assert 'post' in paths['/abilities/attempts/{attemptId}/submit'], "Missing POST .../submit" assert 'get' in paths['/abilities/attempts/{attemptId}/result'], "Missing GET .../result" assert 'get' in paths['/abilities/me'], "Missing GET /abilities/me" print(" V36 ability assessment endpoints present (7 operations / 7 paths)") # ---- V36 严格断言:正好 7 path / 7 operation;无 /abilities/{type}/questions;requestBody+200 schema;无题库内部字段 ---- assert '/abilities/{type}/questions' not in paths, "GET /abilities/{type}/questions must NOT exist" v36_paths = sorted([k for k in paths if k.startswith('/abilities')]) assert len(v36_paths) == 7, f"Expected exactly 7 /abilities paths, got {len(v36_paths)}" v36_ops = sum(len(paths[k]) for k in v36_paths) assert v36_ops == 7, f"Expected exactly 7 /abilities operations, got {v36_ops}" print(f" V36 exactly 7 paths / 7 operations: {v36_paths}") ABILITY_200 = { '/abilities/types': ('get', 'ApiResponseAbilityTypeResponse'), '/abilities/attempts': ('post', 'ApiResponseAbilityAttemptResponse'), '/abilities/attempts/{attemptId}': ('get', 'ApiResponseAbilityAttemptResponse'), '/abilities/attempts/{attemptId}/answers': ('patch', 'ApiResponseAbilityAttemptResponse'), '/abilities/attempts/{attemptId}/submit': ('post', 'ApiResponseAbilityResultResponse'), '/abilities/attempts/{attemptId}/result': ('get', 'ApiResponseAbilityResultResponse'), '/abilities/me': ('get', 'ApiResponsePageResponseAbilityHistoryItem'), } def response_ref(op): return op.get('responses', {}).get('200', {}).get('content', {}).get('application/json', {}).get('schema', {}).get('$ref', '') for pt, (method, expect_ref) in ABILITY_200.items(): ref = response_ref(paths[pt][method]) assert ref.endswith(expect_ref), f"{method.upper()} {pt}: expected 200 ref ...{expect_ref}, got {ref}" print(" All 7 V36 success responses reference a schema") for pt, (method, schema_name) in { '/abilities/attempts': ('post', 'CreateAbilityAttemptRequest'), '/abilities/attempts/{attemptId}/answers': ('patch', 'SaveAbilityAnswersRequest')}.items(): rb = paths[pt][method].get('requestBody', {}).get('content', {}).get('application/json', {}).get('schema', {}).get('$ref', '') assert rb.endswith(schema_name), f"{method.upper()} {pt}: expected requestBody ref ...{schema_name}, got {rb}" print(" POST/PATCH V36 request bodies reference a schema") # 题库内部字段不得出现在 Ability 相关 schema for f in ['questionCode', 'competencyTag', 'answerKey', 'scoringRule']: for name, sch in d['components']['schemas'].items(): if name.startswith('Ability') and f in json.dumps(sch): assert False, f"schema {name} exposes forbidden field {f}" print(" Ability schemas expose no questionCode/competencyTag/answerKey/scoringRule") # V37 风险接口 operation 存在性 assert 'get' in paths['/admin/scoring/risk/overview'], "Missing GET overview" assert 'get' in paths['/admin/scoring/risk/trend'], "Missing GET trend" assert 'get' in paths['/admin/scoring/risk/distribution'], "Missing GET distribution" assert 'get' in paths['/admin/scoring/risk/events'], "Missing GET events" assert 'get' in paths['/admin/scoring/risk/rules'], "Missing GET rules" assert 'put' in paths['/admin/scoring/risk/rules/{ruleCode}'], "Missing PUT rules/{ruleCode}" assert 'post' in paths['/admin/scoring/risk/events/{eventId}/actions'], "Missing POST events/{eventId}/actions" print(" V37 risk endpoints present (7 operations / 7 paths)") # ---- V37 严格断言:正好 7 path / 7 operation;请求/响应 schema;无企业/学生风险接口;Risk schema 无敏感字段 ---- vk = [k for k in paths if k.startswith('/admin/scoring/risk')] assert len(vk) == 7, f"Expected exactly 7 V37 paths, got {len(vk)}" assert sum(len(paths[k]) for k in vk) == 7, "Expected exactly 7 V37 operations" # 不得新增企业/学生风险接口 for k in paths: if k.startswith('/scoring/risk') or k.startswith('/risk'): assert False, f"unexpected student/company risk endpoint {k}" print(f" V37 exactly 7 paths / 7 operations: {vk}") RISK_200 = { '/admin/scoring/risk/overview': 'ApiResponseRiskOverviewResponse', '/admin/scoring/risk/trend': 'ApiResponseRiskTrendResponse', '/admin/scoring/risk/distribution': 'ApiResponseRiskDistributionResponse', '/admin/scoring/risk/events': 'ApiResponsePageResponseRiskEventResponse', '/admin/scoring/risk/rules': 'ApiResponseListRiskRuleResponse', '/admin/scoring/risk/rules/{ruleCode}': 'ApiResponseRiskRuleResponse', '/admin/scoring/risk/events/{eventId}/actions': 'ApiResponseRiskEventResponse', } for pt, expect_ref in RISK_200.items(): method = 'put' if pt.endswith('{ruleCode}') else ('post' if pt.endswith('actions') else 'get') ref = response_ref(paths[pt][method]) assert ref.endswith(expect_ref), f"{method.upper()} {pt}: expected 200 ref ...{expect_ref}, got {ref}" print(" All 7 V37 success responses reference a schema") for pt in ('/admin/scoring/risk/rules/{ruleCode}', '/admin/scoring/risk/events/{eventId}/actions'): method = 'put' if pt.endswith('{ruleCode}') else 'post' rb = paths[pt][method].get('requestBody', {}).get('content', {}).get('application/json', {}).get('schema', {}).get('$ref', '') assert rb != '', f"{method.upper()} {pt} requestBody schema missing" print(" PUT/POST V37 request bodies reference a schema") risk_forbidden = ['subjectUserId', 'subject_user_id', 'operatorUserId', 'operator_user_id', 'phone', 'email', 'ipAddress', 'userAgent', 'details', 'token', 'password', 'secret'] for f in risk_forbidden: for name, sch in d['components']['schemas'].items(): if name.startswith('Risk') and f in json.dumps(sch): assert False, f"schema {name} exposes forbidden field {f}" print(" Risk schemas expose no subjectUserId/operatorUserId/phone/email/ip/userAgent/details/credentials") # V39 测评驱动岗位推荐:6 path / 8 operation;学生反馈、企业画像、ADMIN 审核与公开受控字典。 v39 = [ '/job-recommendation-taxonomies', '/profile/me/job-recommendations', '/profile/me/job-recommendations/{jobId}/feedback', '/companies/me/jobs/{jobId}/recommendation-profile', '/admin/job-recommendation-profiles', '/admin/job-recommendation-profiles/{jobId}/decision', ] assert all(k in paths for k in v39), "One or more V39 paths are missing" assert len(v39) == 6, f"Expected exactly 6 V39 paths, got {len(v39)}" assert sum(len(paths[k]) for k in v39) == 8, "Expected exactly 8 V39 operations" for pt, method in [ ('/job-recommendation-taxonomies', 'get'), ('/profile/me/job-recommendations', 'get'), ('/profile/me/job-recommendations/{jobId}/feedback', 'post'), ('/profile/me/job-recommendations/{jobId}/feedback', 'delete'), ('/companies/me/jobs/{jobId}/recommendation-profile', 'get'), ('/companies/me/jobs/{jobId}/recommendation-profile', 'put'), ('/admin/job-recommendation-profiles', 'get'), ('/admin/job-recommendation-profiles/{jobId}/decision', 'post'), ]: assert method in paths[pt], f"Missing {method.upper()} {pt}" for pt, method in [ ('/profile/me/job-recommendations/{jobId}/feedback', 'post'), ('/companies/me/jobs/{jobId}/recommendation-profile', 'put'), ('/admin/job-recommendation-profiles/{jobId}/decision', 'post'), ]: assert paths[pt][method].get('requestBody', {}).get('content', {}).get('application/json', {}).get('schema', {}).get('$ref'), f"{method.upper()} {pt} requestBody schema missing" recommendation_sensitive = ['answerKey', 'scoringRule', 'questionCode', 'competencyTag', 'resultId', 'attemptId', 'studentUserId'] for f in recommendation_sensitive: for name, sch in d['components']['schemas'].items(): if name.startswith(('JobRecommendation', 'Recommendation')) and f in json.dumps(sch): assert False, f"schema {name} exposes forbidden field {f}" print(" V39 exactly 6 paths / 8 operations; recommendation schemas are redacted") # V40 企业评价官方回复:5 paths / 6 operations;企业、管理员和公开回复均为白名单载荷。 v40 = [ '/companies/me/job-reviews', '/companies/me/job-reviews/{reviewId}', '/companies/me/job-reviews/{reviewId}/reply', '/admin/review-replies/pending', '/admin/review-replies/{replyId}/decision', ] assert all(k in paths for k in v40), "One or more V40 paths are missing" assert len(v40) == 5, f"Expected exactly 5 V40 paths, got {len(v40)}" assert sum(len(paths[k]) for k in v40) == 6, "Expected exactly 6 V40 operations" for pt, method in [ ('/companies/me/job-reviews', 'get'), ('/companies/me/job-reviews/{reviewId}', 'get'), ('/companies/me/job-reviews/{reviewId}/reply', 'post'), ('/companies/me/job-reviews/{reviewId}/reply', 'put'), ('/admin/review-replies/pending', 'get'), ('/admin/review-replies/{replyId}/decision', 'post'), ]: assert method in paths[pt], f"Missing {method.upper()} {pt}" for pt, method in [ ('/companies/me/job-reviews/{reviewId}/reply', 'post'), ('/companies/me/job-reviews/{reviewId}/reply', 'put'), ('/admin/review-replies/{replyId}/decision', 'post'), ]: assert paths[pt][method].get('requestBody', {}).get('content', {}).get('application/json', {}).get('schema', {}).get('$ref'), f"{method.upper()} {pt} requestBody schema missing" for pt, method in [ ('/companies/me/job-reviews', 'get'), ('/companies/me/job-reviews/{reviewId}', 'get'), ('/admin/review-replies/pending', 'get'), ('/companies/me/job-reviews/{reviewId}/reply', 'post'), ('/companies/me/job-reviews/{reviewId}/reply', 'put'), ('/admin/review-replies/{replyId}/decision', 'post'), ]: response = paths[pt][method].get('responses', {}).get('200', {}) assert response.get('content', {}).get('application/json', {}).get('schema', {}).get('$ref'), f"{method.upper()} {pt} 200 schema missing" v40_sensitive = ['studentUserId', 'phone', 'email', 'ipAddress', 'userAgent', 'moderationScore', 'reasonCodes'] for name in ('CompanyReviewItemResponse', 'CompanyReviewDetailResponse', 'CompanyReviewReplyResponse', 'ReviewReplyPageResponse'): assert name in d['components']['schemas'], f"Missing V40 schema {name}" text_schema = json.dumps(d['components']['schemas'][name]) for field in v40_sensitive: assert field not in text_schema, f"V40 company schema {name} exposes {field}" assert 'officialReply' not in json.dumps(d['components']['schemas']['CompanyReviewReplyResponse']) print(" V40 exactly 5 paths / 6 operations; request/response schemas are present and redacted") # 统计 operation 总数并校验 V40 基线 196 operations = sum(len(methods) for methods in d['paths'].values()) print('operations:', operations) assert operations == 196, f"Expected 196 operations, got {operations}" print(" Offer advance endpoints all present") print(" 26 operations all present") print(" Account deletion endpoints present (3 operations)") data = json.dumps(d) assert '"" : "#/components/schemas/' not in data, "Found empty ref key" print("No empty ref keys found") print("Valid JSON: True") print("All checks passed!")