优化
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
| `019fdac1-ae04-7032-bac0-968497ea54ad` | 墨家测量 | `inbound-measurement.mjs` |
|
||||
| `019fd5af-2e85-74b0-94e5-98c1b9ea4d3e` | 船期价格表 | `pricing.mjs` |
|
||||
| `019fdbb1-ca56-7263-8914-a29d96cc5177` | 客户端创建订单 | `orders.mjs`,负责创建隔离测试订单并补 Admin 状态流程 |
|
||||
| `019fdf57-193c-7513-ac22-4ea62adaaf96` | Admin 补充边界验收 | `supplemental.mjs`,只读/负向/保护性契约,不创建或删除历史资源 |
|
||||
|
||||
## 统一交付要求
|
||||
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
// 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' })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user