// Admin HTTP acceptance gaps that are intentionally independent from the CRUD // modules. This module is read-only: it never creates or mutates business data. const HEALTH_ENDPOINTS = [ { path: '/api/platform/v1/orders/status', service: 'order-service' }, { path: '/api/platform/v1/first-leg/status', service: 'firstleg-service' }, { path: '/api/platform/v1/files/status', service: 'file-service' }, { path: '/api/platform/v1/warehouses/status', service: 'warehouse-service', allows503: true }, ] function codeOf(response) { return response?.body?.code == null ? 'missing' : String(response.body.code) } function isMissingRoute(response) { return response?.http === 404 || response?.http === 405 || /Not Found|No handler|No static resource/i.test(String(response?.raw || response?.body?.message || '')) } function isBusinessFailure(response) { const code = codeOf(response) return [400, 401, 403, 404, 409].includes(response?.http) || (response?.http === 200 && code !== '200') } function pageShape(data) { return data && Array.isArray(data.records) && Number(data.current ?? data.page ?? 1) >= 1 && Number(data.size ?? 0) >= 0 } function xlsx(response) { return response?.http === 200 && response.bytes?.length > 4 && response.bytes[0] === 0x50 && response.bytes[1] === 0x4b } async function checkPage(context, module, name, path, options = {}) { const response = await context.request('GET', path) const data = context.apiSuccess(module, `${name}分页查询`, response, `path=${path}`, options) context.assert(module, `${name}分页结构与记录数组正确`, response, pageShape(data), `records=${data?.records?.length ?? 'n/a'}, current=${data?.current ?? data?.page ?? 'n/a'}, size=${data?.size ?? 'n/a'}`, options) return { response, data } } export async function run(context) { const module = 'supplemental' const scenario = 'service-health-and-auth-boundary' const blocker = { scenario, deploymentBlocker: true } // Health endpoints are part of the deployed service contract. Warehouse is // special: a schema/readiness failure is intentionally HTTP 503, not a fake // success, so retain that signal in the report. for (const endpoint of HEALTH_ENDPOINTS) { const response = await context.request('GET', endpoint.path) const notReadyEnvelope = endpoint.allows503 && response.http === 503 && codeOf(response) === '200' const data = notReadyEnvelope ? response.body?.data : context.apiSuccess(module, `${endpoint.service} 状态响应使用统一成功体`, response, `HTTP=${response.http}, code=${codeOf(response)}`, isMissingRoute(response) ? blocker : {}) if (notReadyEnvelope) { context.addStep(module, `${endpoint.service} readiness 失败仍返回统一响应体`, response, true, `HTTP=503, code=${codeOf(response)}, status=${data?.status || 'missing'}`) } const shape = data && data.service === endpoint.service && typeof data.status === 'string' && (endpoint.service !== 'warehouse-service' || typeof data.message === 'string') const ready = response.http === 200 && codeOf(response) === '200' && data?.status === 'UP' const notReady = endpoint.allows503 && response.http === 503 && codeOf(response) === '200' && data && data.service === endpoint.service && data.status !== 'UP' && typeof data.message === 'string' context.assert(module, `${endpoint.service} 状态字段完整且 readiness 语义明确`, response, shape && (ready || notReady), `service=${data?.service || 'missing'}, status=${data?.status || 'missing'}, readiness=${ready ? 'UP' : notReady ? 'NOT_READY' : 'invalid'}`, isMissingRoute(response) ? blocker : {}) } // Gateway must reject both absent and malformed bearer credentials before a // platform handler is reached. This specifically guards against accidental // route-level permitAll changes. No token or response body is persisted. for (const [name, token] of [['未携带令牌', ''], ['伪造令牌', 'invalid-admin-token']]) { const response = await context.request('GET', '/api/platform/v1/orders/status', undefined, token) const rejected = response.http === 401 && ['A0401', '401'].includes(codeOf(response)) context.addStep(module, `平台接口${name}返回 401`, response, rejected, `HTTP=${response.http}, code=${codeOf(response)}`) } // A route that exists but is not protected is an authorization regression; // keep the check separate so a service outage does not look like a pass. const protectedResponse = await context.request('GET', '/api/platform/v1/orders/status', undefined, '') context.assert(module, '未认证请求不返回业务成功码', protectedResponse, !(protectedResponse.http === 200 && codeOf(protectedResponse) === '200'), `HTTP=${protectedResponse.http}, code=${codeOf(protectedResponse)}`) // Warehouse/master-data read-only boundary checks. All resources are // platform-public (tenantId=0); passing a customer scope must fail instead // of silently widening or narrowing the result set. const warehousePage = await checkPage(context, module, '仓库', '/api/platform/v1/warehouse-admin?page=1&size=5&status=ACTIVE', { scenario: 'master-data-readonly' }) const warehouseId = String(warehousePage.data?.records?.[0]?.id || '') if (warehouseId) { await checkPage(context, module, '仓库筛选', `/api/platform/v1/warehouse-admin?page=1&size=5&tenantId=0&warehouseType=${encodeURIComponent(warehousePage.data.records[0].warehouseType || 'DOMESTIC')}`, { scenario: 'master-data-readonly' }) } for (const [name, path] of [ ['仓库非法状态', '/api/platform/v1/warehouse-admin?page=1&size=5&status=NOT_A_STATUS'], ['仓库客户作用域', '/api/platform/v1/warehouse-admin?page=1&size=5&tenantId=10001'], ]) { const response = await context.request('GET', path) context.addStep(module, `${name}安全失败`, response, isBusinessFailure(response), `HTTP=${response.http}, code=${codeOf(response)}`, { scenario: 'master-data-readonly', deploymentBlocker: isMissingRoute(response) }) } const locationBase = '/api/platform/v1/locations?page=1&size=5&tenantId=0' const palletBase = '/api/platform/v1/pallets?page=1&size=5&tenantId=0' const locations = await checkPage(context, module, '库位', locationBase, { scenario: 'warehouse-resources' }) const pallets = await checkPage(context, module, '托盘', palletBase, { scenario: 'warehouse-resources' }) if (warehouseId) { await checkPage(context, module, '库位仓库筛选', `${locationBase}&warehouseId=${warehouseId}`, { scenario: 'warehouse-resources' }) await checkPage(context, module, '托盘仓库筛选', `${palletBase}&warehouseId=${warehouseId}`, { scenario: 'warehouse-resources' }) } const resourceScopeChecks = [ ['库位客户作用域', '/api/platform/v1/locations?page=1&size=5&tenantId=10001'], ['托盘客户作用域', '/api/platform/v1/pallets?page=1&size=5&tenantId=10001'], ['库位非法状态', `${locationBase}&status=ALLOCATED`], ] for (const [name, path] of resourceScopeChecks) { const response = await context.request('GET', path) context.addStep(module, `${name}安全失败`, response, isBusinessFailure(response), `HTTP=${response.http}, code=${codeOf(response)}`, { scenario: 'warehouse-resources', deploymentBlocker: isMissingRoute(response) }) } // A stale version is checked without identifying or mutating an existing // resource; the sentinel ID must be rejected as not found/invalid. for (const [name, path] of [ ['库位过期版本/不存在资源', '/api/platform/v1/locations/999999999999?tenantId=0&warehouseId=999999999999'], ['托盘占用资源保护边界', '/api/platform/v1/pallets/999999999999?tenantId=0&warehouseId=999999999999'], ]) { const response = await context.request('GET', path) context.addStep(module, `${name}不泄露或误修改数据`, response, isBusinessFailure(response), `HTTP=${response.http}, code=${codeOf(response)}`, { scenario: 'warehouse-resources', deploymentBlocker: isMissingRoute(response) }) } // Shared express/partner master data: status/type filters and safe template // downloads. We do not upload arbitrary or real customer workbooks. for (const [label, base, invalid] of [ ['快递公司', '/api/platform/v1/express-companies', 'UNKNOWN'], ['往来单位', '/api/platform/v1/partners', 'UNKNOWN_TYPE'], ]) { await checkPage(context, module, label, `${base}?page=1&size=5&tenantId=0&status=ACTIVE`, { scenario: 'shared-master-data' }) const inactive = await checkPage(context, module, `${label}停用筛选`, `${base}?page=1&size=5&tenantId=0&status=INACTIVE`, { scenario: 'shared-master-data' }) context.assert(module, `${label}状态枚举过滤不混入其他状态`, inactive.response, inactive.data?.records?.every?.((item) => item.status === 'INACTIVE'), `records=${inactive.data?.records?.length ?? 'n/a'}`, { scenario: 'shared-master-data' }) const invalidPath = label === '往来单位' ? `${base}?page=1&size=5&tenantId=0&types=${invalid}` : `${base}?page=1&size=5&tenantId=0&status=${invalid}` const invalidResponse = await context.request('GET', invalidPath) context.addStep(module, `${label}无效筛选参数安全失败`, invalidResponse, isBusinessFailure(invalidResponse), `HTTP=${invalidResponse.http}, code=${codeOf(invalidResponse)}`, { scenario: 'shared-master-data', deploymentBlocker: isMissingRoute(invalidResponse) }) } for (const [label, path] of [ ['仓库导入模板', '/api/platform/v1/warehouse-admin/import-template'], ['快递公司导入模板', '/api/platform/v1/express-companies/import-template'], ['往来单位导入模板', '/api/platform/v1/partners/import-template'], ]) { const response = await context.requestBinary('GET', path) context.addStep(module, `${label}为 XLSX 且不记录二进制`, response, xlsx(response), `HTTP=${response.http}, bytes=${response.bytes?.length ?? 0}, contentType=${response.contentType || 'missing'}`, { scenario: 'shared-master-data', deploymentBlocker: isMissingRoute(response) }) } // Price-book lifecycle is read-only here; pricing.mjs remains the sole owner // of uploads/publication. Verify all supported status filters and source // download evidence from an already existing active/history version. const pricePath = '/api/platform/v1/fba-pricing/carrier-rates/price-books' for (const status of ['PENDING_CONFIRMATION', 'ACTIVE', 'SUPERSEDED']) { const response = await context.request('GET', `${pricePath}?status=${status}`) const versions = context.apiSuccess(module, `船期价格表 ${status} 状态过滤`, response, `count=${Array.isArray(response.body?.data) ? response.body.data.length : 'n/a'}`, { scenario: 'price-book-readonly', deploymentBlocker: isMissingRoute(response) }) context.assert(module, `船期价格表 ${status} 结果状态一致`, response, Array.isArray(versions) && versions.every((item) => item.status === status), `count=${versions?.length ?? 'n/a'}`, { scenario: 'price-book-readonly', deploymentBlocker: isMissingRoute(response) }) } const activeVersionsResponse = await context.request('GET', `${pricePath}?status=ACTIVE`) const activeVersions = context.apiSuccess(module, '读取现有生效价格版本用于下载安全检查', activeVersionsResponse, '', { scenario: 'price-book-readonly', deploymentBlocker: isMissingRoute(activeVersionsResponse) }) const activeVersionId = String(activeVersions?.[0]?.id || '') if (activeVersionId) { const sourceResponse = await context.requestBinary('GET', `${pricePath}/${encodeURIComponent(activeVersionId)}/source`) context.addStep(module, '现有生效价格源文件下载为 XLSX', sourceResponse, xlsx(sourceResponse), `versionId=${activeVersionId}, bytes=${sourceResponse.bytes?.length ?? 0}, contentType=${sourceResponse.contentType || 'missing'}`, { scenario: 'price-book-readonly' }) } else { context.assert(module, '存在可供安全下载核验的生效价格版本', activeVersionsResponse, false, '当前无 ACTIVE 价格版本,无法安全执行源文件下载核验', { scenario: 'price-book-readonly', deploymentBlocker: true }) } // Download center: list/status pagination and negative access checks only; // downloads.mjs owns task creation and successful-file polling. for (const status of ['PENDING', 'RUNNING', 'SUCCEEDED', 'FAILED', 'EXPIRED']) { const response = await context.request('GET', `/api/platform/v1/files/export-tasks?page=1&size=5&status=${status}`) const data = context.apiSuccess(module, `下载中心 ${status} 状态分页`, response, '', { scenario: 'download-readonly', deploymentBlocker: isMissingRoute(response) }) context.assert(module, `下载中心 ${status} 过滤结果状态一致`, response, pageShape(data) && data.records.every((item) => item.status === status), `records=${data?.records?.length ?? 'n/a'}`, { scenario: 'download-readonly', deploymentBlocker: isMissingRoute(response) }) } for (const [name, path] of [ ['不存在下载任务详情', '/api/platform/v1/files/export-tasks/999999999999'], ['不存在下载任务文件', '/api/platform/v1/files/export-tasks/999999999999/download'], ]) { const response = await context.request('GET', path, undefined, context.token) context.addStep(module, `${name}安全失败`, response, isBusinessFailure(response), `HTTP=${response.http}, code=${codeOf(response)}`, { scenario: 'download-readonly', deploymentBlocker: isMissingRoute(response) }) } const unauthDownload = await context.request('GET', '/api/platform/v1/files/export-tasks/999999999999/download', undefined, '') context.addStep(module, '未授权下载请求被拒绝', unauthDownload, unauthDownload.http === 401, `HTTP=${unauthDownload.http}, code=${codeOf(unauthDownload)}`, { scenario: 'download-readonly' }) // IAM lists are intentionally read-only. The RBAC endpoints are list-shaped // arrays (not paginated in the current contract); retain that fact instead of // pretending pagination exists, and verify built-in markers are exposed. for (const [label, path, builtInField] of [ ['平台用户', '/api/platform/v1/iam/users?page=1&size=5', 'protectedAccount'], ['平台角色', '/api/platform/v1/iam/roles', 'roleType'], ['菜单权限', '/api/platform/v1/iam/permissions', 'builtIn'], ]) { const response = await context.request('GET', path) const data = context.apiSuccess(module, `${label}只读列表`, response, '', { scenario: 'iam-readonly', deploymentBlocker: isMissingRoute(response) }) const records = label === '平台用户' ? data?.records : data context.assert(module, `${label}列表结构正确且包含内置保护标识`, response, label === '平台用户' ? pageShape(data) : Array.isArray(records) && records.some((item) => label === '平台角色' ? item.roleType === 'SYSTEM' : Object.hasOwn(item, builtInField)), `records=${records?.length ?? 'n/a'}, builtInField=${builtInField}`, { scenario: 'iam-readonly', deploymentBlocker: isMissingRoute(response) }) } for (const [name, path] of [ ['角色列表伪造令牌', '/api/platform/v1/iam/roles'], ['菜单权限未携带令牌', '/api/platform/v1/iam/permissions'], ]) { const response = await context.request('GET', path, undefined, name.includes('伪造') ? 'invalid-admin-token' : '') context.addStep(module, `${name}拒绝访问`, response, response.http === 401, `HTTP=${response.http}, code=${codeOf(response)}`, { scenario: 'iam-readonly' }) } }