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.
318 lines
20 KiB
318 lines
20 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
|
|
) == 172, 'Expected 172 operations after N04 onboarding'
|
|
|
|
assert len(d['paths']) == 135, f"Expected 135 paths, got {len(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',
|
|
'/skills/word-cloud',
|
|
'/skills/match',
|
|
'/skills/learning-path',
|
|
]
|
|
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}'
|
|
)
|
|
|
|
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!")
|