You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

351 lines
22 KiB

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!")