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'])) paths = d['paths'] onboarding_paths = [ '/onboarding/companies/{companyId}/checklist', '/onboarding/companies/{companyId}/checklist/items/{itemId}/completion', '/onboarding/companies/{companyId}/knowledge', '/onboarding/company/checklist-items', '/onboarding/company/checklist-items/{itemId}', '/onboarding/company/knowledge', ] for path in onboarding_paths: assert path in paths, f'Missing onboarding path: {path}' onboarding_operations = { ('/onboarding/companies/{companyId}/checklist', 'get'): 'getEffectiveChecklist', ('/onboarding/companies/{companyId}/checklist/items/{itemId}/completion', 'put'): 'updateCompletion', ('/onboarding/companies/{companyId}/knowledge', 'get'): 'getCompanyKnowledgeOverview', ('/onboarding/company/checklist-items', 'get'): 'listCompanyChecklistItems', ('/onboarding/company/checklist-items', 'post'): 'createCompanyChecklistItem', ('/onboarding/company/checklist-items/{itemId}', 'put'): 'updateCompanyChecklistItem', ('/onboarding/company/checklist-items/{itemId}', 'delete'): 'deleteCompanyChecklistItem', ('/onboarding/company/knowledge', 'get'): 'getCompanyKnowledge', ('/onboarding/company/knowledge', 'put'): 'upsertCompanyKnowledge', } http_methods = {'get', 'post', 'put', 'patch', 'delete', 'options', 'head', 'trace'} for (path, method), operation_id in onboarding_operations.items(): assert method in paths[path], f'Missing {method.upper()} {path}' assert paths[path][method].get('operationId') == operation_id, f'Wrong operationId for {method.upper()} {path}' matching_operations = [ (candidate_method, candidate_path) for candidate_path, path_item in paths.items() for candidate_method, operation in path_item.items() if candidate_method in http_methods and operation.get('operationId') == operation_id ] assert matching_operations == [(method, path)], ( f'{operation_id} must be globally unique: expected {method.upper()} {path}, got {matching_operations}' ) assert paths[path][method].get('security') == [{'bearerAuth': []}], ( f'{method.upper()} {path} must require bearerAuth' ) assert sum( 1 for path_item in paths.values() for method in path_item if method in http_methods ) == 178, 'Expected 178 operations after N05 salary calculator' assert len(d['paths']) == 139, f"Expected 139 paths, got {len(d['paths'])}" required_salary_paths = ['/salary-calculator/cities','/salary-calculator/cities/{cityCode}','/salary-calculator/calculations','/salary-calculator/calculations/{id}'] for path in required_salary_paths: assert path in paths, f'Missing salary path: {path}' salary_operations = { ('/salary-calculator/cities', 'get'): 'listSalaryCities', ('/salary-calculator/cities/{cityCode}', 'get'): 'getSalaryCity', ('/salary-calculator/calculations', 'post'): 'calculateSalary', ('/salary-calculator/calculations', 'get'): 'listSalaryCalculations', ('/salary-calculator/calculations/{id}', 'get'): 'getSalaryCalculation', ('/salary-calculator/calculations/{id}', 'delete'): 'deleteSalaryCalculation', } for (path, method), operation_id in salary_operations.items(): assert method in paths[path], f'Missing {method.upper()} {path}' assert paths[path][method].get('operationId') == operation_id, f'Wrong operationId for {method.upper()} {path}' assert paths[path][method].get('security') == [{'bearerAuth': []}], f'{method.upper()} {path} must require bearerAuth' 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', '/skills/word-cloud', '/skills/match', '/skills/learning-path', '/salary-calculator/cities', '/salary-calculator/cities/{cityCode}', '/salary-calculator/calculations', '/salary-calculator/calculations/{id}', ] 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)") # Skill cloud endpoints (3 paths / 3 GET operations) for skill_path in ('/skills/word-cloud', '/skills/match', '/skills/learning-path'): assert 'get' in paths[skill_path], f"Missing GET {skill_path}" skill_operation_ids = { '/skills/word-cloud': 'getWordCloud', '/skills/match': 'getSkillMatch', '/skills/learning-path': 'getLearningPath', } for skill_path, skill_operation_id in skill_operation_ids.items(): assert paths[skill_path]['get'].get('operationId') == skill_operation_id http_methods = {'get', 'post', 'put', 'patch', 'delete', 'options', 'head', 'trace'} matching_operations = [ (method, path) for path, path_item in paths.items() for method, operation in path_item.items() if method in http_methods and operation.get('operationId') == skill_operation_id ] assert matching_operations == [('get', skill_path)], ( f'{skill_operation_id} must not conflict: expected GET {skill_path}, got {matching_operations}' ) assert paths['/skills/word-cloud']['get']['responses']['200']['content']['application/json']['schema']['$ref'] == '#/components/schemas/ApiResponseSkillCloudResponse' assert paths['/skills/match']['get']['responses']['200']['content']['application/json']['schema']['$ref'] == '#/components/schemas/ApiResponseSkillMatchResponse' assert paths['/skills/learning-path']['get']['responses']['200']['content']['application/json']['schema']['$ref'] == '#/components/schemas/ApiResponseSkillLearningPathResponse' for skill_path in ('/skills/word-cloud', '/skills/match', '/skills/learning-path'): unauthorized = paths[skill_path]['get']['responses']['401'] assert unauthorized['description'] == '未登录或令牌无效,由全局 Security 入口返回裸 JSON' assert unauthorized['content']['application/json']['schema']['$ref'] == '#/components/schemas/UnauthorizedResponse' example = unauthorized['content']['application/json']['example'] assert example == {'code': 401, 'message': 'not authenticated', 'timestamp': ''} assert 'requestId' not in example schemas = d['components']['schemas'] unauthorized_schema = schemas.get('UnauthorizedResponse') assert unauthorized_schema is not None, 'Missing schema: UnauthorizedResponse' assert unauthorized_schema.get('additionalProperties') is False assert set(unauthorized_schema.get('required', [])) == {'code', 'message', 'timestamp'} assert set(unauthorized_schema.get('properties', {})) == {'code', 'message', 'timestamp'} skill_schema_fields = { 'SkillCloudNode': {'id', 'code', 'name'}, 'SkillCloudItem': {'rank', 'skillId', 'skillCode', 'nameZh', 'nameEn', 'sourceType', 'sourceCode', 'categoryId', 'categoryName', 'heat', 'heatSourceCode', 'heatElementId', 'heatProxy'}, 'SkillCloudResponse': {'industry', 'occupation', 'category', 'total', 'items'}, 'SkillMatchResponse': {'matchScore', 'matchedCount', 'totalCount', 'matchedSkills', 'missingSkills'}, 'SkillLearningPathItem': {'priority', 'skillId', 'skillName', 'categoryId', 'categoryName', 'suggestion'}, 'SkillLearningPathResponse': {'total', 'items'}, } for schema_name, expected_fields in skill_schema_fields.items(): schema = schemas.get(schema_name) assert schema is not None, f'Missing schema: {schema_name}' assert expected_fields <= set(schema.get('properties', {})), f'Schema fields missing: {schema_name}' word_cloud_example = paths['/skills/word-cloud']['get']['responses']['200']['content']['application/json']['example']['data'] assert word_cloud_example['total'] == len(word_cloud_example['items']) match_example = paths['/skills/match']['get']['responses']['200']['content']['application/json']['example']['data'] assert match_example['matchedCount'] == len(match_example['matchedSkills']) assert match_example['totalCount'] == len(match_example['matchedSkills']) + len(match_example['missingSkills']) assert match_example['matchScore'] == round(match_example['matchedCount'] * 100 / match_example['totalCount']) learning_path_example = paths['/skills/learning-path']['get']['responses']['200']['content']['application/json']['example']['data'] assert learning_path_example['total'] == len(learning_path_example['items']) print(" Skill cloud endpoints and schemas present (3 GET operations)") print(" Offer advance endpoints all present") print(" 26 operations all present") print(" Account deletion endpoints present (3 operations)") onboarding_success_refs = { ('/onboarding/companies/{companyId}/checklist', 'get'): 'ApiResponseEffectiveChecklistResponse', ('/onboarding/companies/{companyId}/checklist/items/{itemId}/completion', 'put'): 'ApiResponseProgressUpdateResponse', ('/onboarding/companies/{companyId}/knowledge', 'get'): 'ApiResponseCompanyKnowledgeOverviewResponse', ('/onboarding/company/checklist-items', 'get'): 'ApiResponseListCompanyChecklistItemView', ('/onboarding/company/checklist-items', 'post'): 'ApiResponseCompanyChecklistItemView', ('/onboarding/company/checklist-items/{itemId}', 'put'): 'ApiResponseCompanyChecklistItemView', ('/onboarding/company/checklist-items/{itemId}', 'delete'): 'ApiResponseCompanyChecklistItemView', ('/onboarding/company/knowledge', 'get'): 'ApiResponseCompanyKnowledgeView', ('/onboarding/company/knowledge', 'put'): 'ApiResponseCompanyKnowledgeView', } for (path, method), schema_name in onboarding_success_refs.items(): response = paths[path][method]['responses']['200'] ref = response['content']['*/*']['schema']['$ref'] assert ref == f'#/components/schemas/{schema_name}', f'Wrong success schema for {method.upper()} {path}: {ref}' for status_code in ('401', '403'): assert status_code in paths[path][method]['responses'], f'Missing {status_code} for {method.upper()} {path}' unauthorized = paths[path][method]['responses']['401'] assert unauthorized['content']['application/json']['schema']['$ref'] == '#/components/schemas/UnauthorizedResponse' forbidden = paths[path][method]['responses']['403'] assert forbidden['content']['application/json']['schema']['$ref'] == '#/components/schemas/ApiResponse' assert forbidden['content']['application/json']['example']['code'] == 2002 onboarding_schema_fields = { 'CompanyChecklistItemCreateRequest': {'title', 'description', 'category', 'sortOrder', 'required', 'idempotencyKey'}, 'CompanyChecklistItemUpdateRequest': {'title', 'description', 'category', 'sortOrder', 'required', 'active', 'version'}, 'CompanyChecklistItemView': {'itemId', 'source', 'itemKey', 'title', 'description', 'category', 'required', 'sortOrder', 'active', 'version'}, 'EffectiveChecklistResponse': {'companyId', 'companyName', 'customized', 'totalItems', 'completedItems', 'completionRate', 'items'}, 'ChecklistItemView': {'itemId', 'source', 'itemKey', 'title', 'description', 'category', 'required', 'sortOrder', 'active', 'completed', 'completedAt', 'progressVersion'}, 'ProgressUpdateRequest': {'completed', 'version'}, 'ProgressUpdateResponse': {'itemId', 'completed', 'completedAt', 'progressVersion', 'totalItems', 'completedItems', 'completionRate'}, 'CompanyKnowledgeUpdateRequest': {'culture', 'workPolicies', 'onboardingGuide', 'version'}, 'CompanyKnowledgeView': {'configured', 'culture', 'workPolicies', 'onboardingGuide', 'updatedAt', 'version'}, 'CompanyKnowledgeOverviewResponse': {'company', 'knowledgeState', 'knowledge', 'reviewSummary'}, 'CompanyView': {'id', 'name', 'logoUrl', 'description', 'industry', 'website', 'address', 'verified'}, 'KnowledgeView': {'culture', 'workPolicies', 'onboardingGuide', 'updatedAt', 'version'}, 'ReviewSummaryView': {'state', 'totalReviews', 'averageRating', 'ratingDistribution', 'topTags', 'dimensionSummary'}, 'DimensionSummaryView': {'averageScore', 'sampleCount', 'dataState'}, } for schema_name, expected_fields in onboarding_schema_fields.items(): schema = schemas.get(schema_name) assert schema is not None, f'Missing onboarding schema: {schema_name}' assert expected_fields <= set(schema.get('properties', {})), ( f'Onboarding schema fields missing: {schema_name}' ) create_request = schemas['CompanyChecklistItemCreateRequest']['properties'] assert create_request['sortOrder'].get('default') == 1000, ( 'CompanyChecklistItemCreateRequest.sortOrder default must be 1000' ) assert create_request['required'].get('default') is False, ( 'CompanyChecklistItemCreateRequest.required default must be false' ) knowledge_success_example = paths['/onboarding/company/knowledge']['put']['responses']['200']['content']['*/*']['example'] assert knowledge_success_example['data'].get('version') == 2, ( 'PUT /onboarding/company/knowledge 200 example data.version must be 2' ) onboarding_error_paths = { 9701: ('/onboarding/company/checklist-items/{itemId}', 'put', '404'), 9702: ('/onboarding/company/checklist-items/{itemId}', 'put', '409'), 9703: ('/onboarding/company/checklist-items', 'post', '409'), 9704: ('/onboarding/company/checklist-items', 'post', '409'), 9705: ('/onboarding/companies/{companyId}/checklist/items/{itemId}/completion', 'put', '409'), 9706: ('/onboarding/company/knowledge', 'put', '409'), } for error_code, (path, method, status_code) in onboarding_error_paths.items(): response_text = json.dumps(paths[path][method]['responses'][status_code], ensure_ascii=False) assert str(error_code) in response_text, f'Missing onboarding error code {error_code} in {method.upper()} {path}' print(' N04 onboarding endpoints and schemas present (9 operations / 6 paths)') 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!")