import { test } from 'node:test' import assert from 'node:assert/strict' import { API_ENDPOINTS } from '../src/shared/api/endpoints.ts' const MODULE_KEYS = [ 'files', 'dedupe', 'split', 'convert', 'publish', 'deleteBrand', 'productRisk', 'shopMatch', 'patrolDelete', 'queryAsin', 'shopDataCrawl', 'withdraw', 'priceTrack', 'similarAsin', 'appearancePatent', 'collectData', 'imageVideo', 'brand', 'permission', 'digitalHuman', ] function allEndpointValues(): string[] { const values: string[] = [] for (const moduleKey of Object.keys(API_ENDPOINTS)) { const group = (API_ENDPOINTS as Record>)[moduleKey] for (const value of Object.values(group)) { values.push(value) } } return values } test('test_endpoints_all_modules_present', () => { for (const moduleKey of MODULE_KEYS) { assert.ok( Object.prototype.hasOwnProperty.call(API_ENDPOINTS, moduleKey), `缺少模块分组: ${moduleKey}`, ) assert.ok( Object.keys((API_ENDPOINTS as Record>)[moduleKey]).length > 0, `模块分组为空: ${moduleKey}`, ) } }) test('test_endpoint_path_format', () => { for (const value of allEndpointValues()) { assert.ok( value.startsWith('/api/'), `端点应以 /api/ 开头: ${value}`, ) } }) test('test_no_newapi_prefix_in_endpoint', () => { for (const value of allEndpointValues()) { assert.ok( !value.includes('/newApi'), `端点不应包含 /newApi 前缀: ${value}`, ) } }) test('test_progress_batch_paths_match_controllers', () => { const progressPaths = allEndpointValues().filter((value) => value.endsWith('/tasks/progress/batch'), ) assert.deepEqual( [...progressPaths].sort(), [ '/api/similar-asin/tasks/progress/batch', '/api/appearance-patent/tasks/progress/batch', '/api/collect-data/tasks/progress/batch', '/api/publish/tasks/progress/batch', '/api/delete-brand/tasks/progress/batch', '/api/patrol-delete/tasks/progress/batch', '/api/price-track/tasks/progress/batch', '/api/product-risk-resolve/tasks/progress/batch', '/api/shop-match/tasks/progress/batch', '/api/query-asin/tasks/progress/batch', '/api/shop-data-crawl/tasks/progress/batch', '/api/withdraw/tasks/progress/batch', ].sort(), 'progress/batch 端点应与 Java controller 路由一一对应', ) }) test('test_download_paths_match', () => { const resultDownloads = allEndpointValues().filter((value) => value.endsWith('/download'), ) assert.ok(resultDownloads.length >= 12, `resultDownload 端点数量不足: ${resultDownloads.length}`) for (const value of resultDownloads) { assert.ok( /\/results\/\{[a-zA-Z]+\}\/download$/.test(value) || /\/tasks\/\{[a-zA-Z]+\}\/download$/.test(value), `download 端点应匹配 /results/{id}/download 或 /tasks/{id}/download 模式: ${value}`, ) } }) test('test_endpoints_frozen_snapshot', () => { assert.deepEqual(API_ENDPOINTS, { files: { upload: '/api/files/upload', excelInfo: '/api/files/excel-info', }, dedupe: { run: '/api/dedupe/run', history: '/api/dedupe/history', historyDelete: '/api/dedupe/history/{resultId}', resultDownload: '/api/dedupe/results/{resultId}/download', }, split: { run: '/api/split/run', history: '/api/split/history', historyDelete: '/api/split/history/{resultId}', resultDownload: '/api/split/results/{resultId}/download', }, convert: { run: '/api/convert/run', history: '/api/convert/history', historyDelete: '/api/convert/history/{resultId}', templates: '/api/convert/templates', templatesImport: '/api/convert/templates/import', templateDefault: '/api/convert/templates/{templateCode}/default', templateDelete: '/api/convert/templates/{templateCode}', resultDownload: '/api/convert/results/{resultId}/download', }, publish: { parse: '/api/publish/parse', taskActivate: '/api/publish/tasks/{taskId}/activate', fileActivate: '/api/publish/tasks/{taskId}/files/{fileId}/activate', items: '/api/publish/tasks/{taskId}/items', progressBatch: '/api/publish/tasks/progress/batch', result: '/api/publish/tasks/{taskId}/result', dashboard: '/api/publish/dashboard', history: '/api/publish/history', taskDelete: '/api/publish/tasks/{taskId}', }, deleteBrand: { run: '/api/delete-brand/run', history: '/api/delete-brand/history', historyDelete: '/api/delete-brand/history/{resultId}', taskDetail: '/api/delete-brand/tasks/{taskId}', taskDetails: '/api/delete-brand/tasks/batch', taskDownload: '/api/delete-brand/tasks/{taskId}/download', resultDownload: '/api/delete-brand/results/{resultId}/download', resultSubmit: '/api/delete-brand/tasks/{taskId}/result', progressBatch: '/api/delete-brand/tasks/progress/batch', }, productRisk: { candidates: '/api/product-risk-resolve/candidates', candidateAdd: '/api/product-risk-resolve/candidates', candidateDelete: '/api/product-risk-resolve/candidates/{id}', countryPreference: '/api/product-risk-resolve/country-preference', matchShops: '/api/product-risk-resolve/match-shops', dashboard: '/api/product-risk-resolve/dashboard', history: '/api/product-risk-resolve/history', historyDelete: '/api/product-risk-resolve/history/{resultId}', taskCreate: '/api/product-risk-resolve/tasks', taskDelete: '/api/product-risk-resolve/tasks/{taskId}', taskBatch: '/api/product-risk-resolve/tasks/batch', progressBatch: '/api/product-risk-resolve/tasks/progress/batch', pendingShopResult: '/api/product-risk-resolve/pending-shop-result', resultDownload: '/api/product-risk-resolve/results/{resultId}/download', }, shopMatch: { candidates: '/api/shop-match/candidates', candidateAdd: '/api/shop-match/candidates', candidateDelete: '/api/shop-match/candidates/{id}', countryPreference: '/api/shop-match/country-preference', matchShops: '/api/shop-match/match-shops', dashboard: '/api/shop-match/dashboard', history: '/api/shop-match/history', historyDelete: '/api/shop-match/history/{resultId}', taskCreate: '/api/shop-match/tasks', taskDelete: '/api/shop-match/tasks/{taskId}', taskActivate: '/api/shop-match/tasks/{taskId}/activate', stageFinished: '/api/shop-match/tasks/{taskId}/stage-finished', taskBatch: '/api/shop-match/tasks/batch', progressBatch: '/api/shop-match/tasks/progress/batch', skipAsins: '/api/shop-match/tasks/{taskId}/skip-asins/paginated', resultDownload: '/api/shop-match/results/{resultId}/download', }, patrolDelete: { candidates: '/api/patrol-delete/candidates', candidateAdd: '/api/patrol-delete/candidates', candidateDelete: '/api/patrol-delete/candidates/{id}', conditions: '/api/patrol-delete/conditions', conditionAdd: '/api/patrol-delete/conditions', conditionDelete: '/api/patrol-delete/conditions/{id}', matchShops: '/api/patrol-delete/match-shops', dashboard: '/api/patrol-delete/dashboard', history: '/api/patrol-delete/history', historyDelete: '/api/patrol-delete/history/{resultId}', progressBatch: '/api/patrol-delete/tasks/progress/batch', taskCreate: '/api/patrol-delete/tasks', taskDelete: '/api/patrol-delete/tasks/{taskId}', taskResult: '/api/patrol-delete/tasks/{taskId}/result', resultDownload: '/api/patrol-delete/results/{resultId}/download', }, queryAsin: { candidates: '/api/query-asin/candidates', candidateAdd: '/api/query-asin/candidates', candidateDelete: '/api/query-asin/candidates/{id}', matchShops: '/api/query-asin/match-shops', dashboard: '/api/query-asin/dashboard', history: '/api/query-asin/history', historyDelete: '/api/query-asin/history/{resultId}', progressBatch: '/api/query-asin/tasks/progress/batch', taskCreate: '/api/query-asin/tasks', taskDelete: '/api/query-asin/tasks/{taskId}', taskResult: '/api/query-asin/tasks/{taskId}/result', resultDownload: '/api/query-asin/results/{resultId}/download', }, shopDataCrawl: { candidates: '/api/shop-data-crawl/candidates', candidateAdd: '/api/shop-data-crawl/candidates', candidateDelete: '/api/shop-data-crawl/candidates/{id}', countryPreference: '/api/shop-data-crawl/country-preference', matchShops: '/api/shop-data-crawl/match-shops', dashboard: '/api/shop-data-crawl/dashboard', history: '/api/shop-data-crawl/history', historyDelete: '/api/shop-data-crawl/history/{resultId}', progressBatch: '/api/shop-data-crawl/tasks/progress/batch', taskCreate: '/api/shop-data-crawl/tasks', taskDelete: '/api/shop-data-crawl/tasks/{taskId}', resultDownload: '/api/shop-data-crawl/results/{resultId}/download', }, withdraw: { candidates: '/api/withdraw/candidates', candidateAdd: '/api/withdraw/candidates', candidateDelete: '/api/withdraw/candidates/{id}', candidatesClear: '/api/withdraw/candidates/clear', matchShops: '/api/withdraw/match-shops', dashboard: '/api/withdraw/dashboard', history: '/api/withdraw/history', historyDelete: '/api/withdraw/history/{resultId}', progressBatch: '/api/withdraw/tasks/progress/batch', taskCreate: '/api/withdraw/tasks', taskDelete: '/api/withdraw/tasks/{taskId}', taskResult: '/api/withdraw/tasks/{taskId}/result', resultDownload: '/api/withdraw/results/{resultId}/download', }, priceTrack: { candidates: '/api/price-track/candidates', candidateAdd: '/api/price-track/candidates', candidateDelete: '/api/price-track/candidates/{id}', countryPreference: '/api/price-track/country-preference', matchShops: '/api/price-track/match-shops', taskCreate: '/api/price-track/tasks', taskDelete: '/api/price-track/tasks/{taskId}', tasksBatch: '/api/price-track/tasks/batch', progressBatch: '/api/price-track/tasks/progress/batch', loopRuns: '/api/price-track/loop-runs', loopRunDetail: '/api/price-track/loop-runs/{loopRunId}', loopRunStop: '/api/price-track/loop-runs/{loopRunId}/stop', loopRunDispatchNext: '/api/price-track/loop-runs/{loopRunId}/dispatch-next', loopRunChildFinished: '/api/price-track/loop-runs/{loopRunId}/child-finished', skipAsinCheck: '/api/price-track/tasks/{taskId}/skip-asin/check', skipAsins: '/api/price-track/tasks/{taskId}/skip-asins/paginated', pendingShopResult: '/api/price-track/pending-shop-result', dispatchFailed: '/api/price-track/tasks/{taskId}/dispatch-failed', dashboard: '/api/price-track/dashboard', history: '/api/price-track/history', historyDelete: '/api/price-track/history/{resultId}', resultDownload: '/api/price-track/results/{resultId}/download', }, similarAsin: { parse: '/api/similar-asin/parse', dashboard: '/api/similar-asin/dashboard', history: '/api/similar-asin/history', historyDelete: '/api/similar-asin/history/{resultId}', progressBatch: '/api/similar-asin/tasks/progress/batch', taskActivate: '/api/similar-asin/tasks/{taskId}/activate', taskDelete: '/api/similar-asin/tasks/{taskId}', resultDownload: '/api/similar-asin/results/{resultId}/download', }, appearancePatent: { parse: '/api/appearance-patent/parse', parsedPayload: '/api/appearance-patent/tasks/{taskId}/parsed-payload', queuePayload: '/api/appearance-patent/tasks/{taskId}/queue-payload', dashboard: '/api/appearance-patent/dashboard', history: '/api/appearance-patent/history', historyDelete: '/api/appearance-patent/history/{resultId}', progressBatch: '/api/appearance-patent/tasks/progress/batch', taskActivate: '/api/appearance-patent/tasks/{taskId}/activate', taskDelete: '/api/appearance-patent/tasks/{taskId}', resultDownload: '/api/appearance-patent/results/{resultId}/download', }, collectData: { parse: '/api/collect-data/parse', countryPreference: '/api/collect-data/country-preference', dashboard: '/api/collect-data/dashboard', history: '/api/collect-data/history', historyDelete: '/api/collect-data/history/{resultId}', progressBatch: '/api/collect-data/tasks/progress/batch', taskActivate: '/api/collect-data/tasks/{taskId}/activate', taskDelete: '/api/collect-data/tasks/{taskId}', fail: '/api/collect-data/tasks/{taskId}/fail', items: '/api/collect-data/tasks/{taskId}/items', resultDownload: '/api/collect-data/results/{resultId}/download', }, imageVideo: { workflowRun: '/api/image-video/workflow/run', workflowResult: '/api/image-video/workflow/result', secrets: '/api/image-video/secrets', taskDetail: '/api/image-video/tasks/{taskId}', voiceClone: '/api/image-video/voice/clone', voiceList: '/api/image-video/voice/list', voiceDelete: '/api/image-video/voice/delete', voiceSynthesis: '/api/image-video/voice/synthesis', douyinCopy: '/api/image-video/douyin-copy', }, brand: { run: '/api/brand/run', expandFolder: '/api/brand/expand-folder', expandFolderRecursive: '/api/brand/expand-folder-recursive', taskDetail: '/api/brand/tasks/{taskId}', taskCancel: '/api/brand/tasks/{taskId}/cancel', taskDownload: '/api/brand/tasks/{taskId}/download', taskEvents: '/api/brand/tasks/{taskId}/events', }, permission: { columnPermissions: '/api/admin/permission-users/{uid}/column-permissions', }, digitalHuman: { versions: '/api/digital-human/versions', upload: '/api/digital-human/versions/upload', latest: '/api/digital-human/versions/latest', detail: '/api/digital-human/versions/{version}', release: '/api/digital-human/versions/{version}/release', setLatest: '/api/digital-human/versions/{version}/set-latest', delete: '/api/digital-human/versions/{version}', downloadUrl: '/api/digital-human/versions/{version}/download-url', }, }, '常量表快照不应漂移') }) test('test_endpoint_values_no_trailing_slash', () => { for (const value of allEndpointValues()) { assert.ok( !value.endsWith('/'), `端点不应以斜杠结尾: ${value}`, ) } }) test('test_endpoint_keys_convention', () => { for (const [moduleKey, group] of Object.entries(API_ENDPOINTS)) { assert.ok(/^[a-z][a-zA-Z0-9]*$/.test(moduleKey), `模块键应为小驼峰: ${moduleKey}`) for (const key of Object.keys(group as Record)) { assert.ok( /^[a-z][a-zA-Z0-9]*$/.test(key), `端点键应为小驼峰: ${moduleKey}.${key}`, ) } } })