优化
This commit is contained in:
@@ -4,6 +4,20 @@ const CONTAINERS = '/api/platform/v1/containers'
|
||||
|
||||
function id(value) { return value == null ? '' : String(value) }
|
||||
|
||||
export function controlledPhotoDownload(downloadPath, response) {
|
||||
const controlledPath = typeof downloadPath === 'string'
|
||||
&& downloadPath.startsWith('/api/')
|
||||
&& !downloadPath.startsWith('//')
|
||||
&& !downloadPath.includes('://')
|
||||
const contentType = String(response?.contentType || '').toLowerCase()
|
||||
return controlledPath
|
||||
&& response?.http === 200
|
||||
&& response?.bytes?.length > 0
|
||||
&& Boolean(contentType)
|
||||
&& !contentType.includes('json')
|
||||
&& !contentType.includes('text/html')
|
||||
}
|
||||
|
||||
export async function run(context) {
|
||||
const loadingRequested = context.options?.modules?.includes('phase3-loading') === true
|
||||
const fixture = loadingRequested
|
||||
@@ -39,7 +53,7 @@ export async function run(context) {
|
||||
if (overview?.pda) {
|
||||
const expectsPhotos = fixture === context.createdResources?.phase3Loading
|
||||
&& Array.isArray(fixture?.evidenceTypes) && fixture.evidenceTypes.length > 0
|
||||
const requiredFields = ['taskId', 'taskStatus', 'taskVersion', 'itemCount', 'completedItemCount', 'unresolvedItemCount']
|
||||
const requiredFields = ['taskId', 'taskStatus', 'workflowStatus', 'taskVersion', 'itemCount', 'completedItemCount', 'unresolvedItemCount']
|
||||
const missingFields = requiredFields.filter((field) => overview.pda[field] === undefined)
|
||||
context.assert(MODULE, 'overview PDA 摘要字段完整', overviewResponse,
|
||||
missingFields.length === 0
|
||||
@@ -48,10 +62,29 @@ export async function run(context) {
|
||||
&& Array.isArray(overview.pda.exceptionReasons)
|
||||
&& (!expectsPhotos || overview.pda.photos.length > 0),
|
||||
`taskId=${id(overview.pda.taskId)}, photos=${overview.pda.photos?.length ?? 'not-array'}, missingFields=${missingFields.join('|') || 'none'}, photoCountsArray=${Array.isArray(overview.pda.photoCounts)}, photosArray=${Array.isArray(overview.pda.photos)}, exceptionReasonsArray=${Array.isArray(overview.pda.exceptionReasons)}`, OPTIONS)
|
||||
const progressIsComplete = Number(overview.pda.itemCount) > 0
|
||||
&& Number(overview.pda.completedItemCount) === Number(overview.pda.itemCount)
|
||||
&& Number(overview.pda.unresolvedItemCount) === 0
|
||||
context.assert(MODULE, 'overview PDA 100%进度区分作业中与待复核', overviewResponse,
|
||||
!progressIsComplete || overview.pda.taskStatus !== 'LOADING'
|
||||
|| overview.pda.workflowStatus === 'AWAITING_REVIEW',
|
||||
`taskStatus=${overview.pda.taskStatus || 'missing'}, workflowStatus=${overview.pda.workflowStatus || 'missing'}, completed=${overview.pda.completedItemCount ?? 'missing'}/${overview.pda.itemCount ?? 'missing'}`, OPTIONS)
|
||||
for (const [index, photo] of overview.pda.photos.entries()) {
|
||||
context.assert(MODULE, `overview PDA 照片摘要 ${index + 1} 字段完整`, overviewResponse,
|
||||
['evidenceId', 'fileId', 'status', 'downloadPath'].every((field) => photo[field] !== undefined),
|
||||
`fileId=${id(photo.fileId)}`, OPTIONS)
|
||||
const downloadPath = String(photo.downloadPath || '')
|
||||
const controlledPath = downloadPath.startsWith('/api/')
|
||||
&& !downloadPath.startsWith('//') && !downloadPath.includes('://')
|
||||
if (!controlledPath) {
|
||||
context.assert(MODULE, `overview PDA 照片 ${index + 1} 下载路径受控`, overviewResponse, false,
|
||||
`fileId=${id(photo.fileId)}, downloadPath=${downloadPath || 'missing'}`, OPTIONS)
|
||||
continue
|
||||
}
|
||||
const download = await context.requestBinary('GET', downloadPath)
|
||||
context.assert(MODULE, `overview PDA 照片 ${index + 1} 可鉴权下载`, download,
|
||||
controlledPhotoDownload(downloadPath, download),
|
||||
`fileId=${id(photo.fileId)}, bytes=${download?.bytes?.length ?? 0}, contentType=${download?.contentType || 'missing'}`, OPTIONS)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import test from 'node:test'
|
||||
|
||||
import { controlledPhotoDownload } from './phase3-container-overview.mjs'
|
||||
|
||||
const read = (path) => readFileSync(new URL(path, import.meta.url), 'utf8')
|
||||
const overviewOpenApi = read('../../firstleg/src/main/resources/openapi/phase3-container-overview-v1.yaml')
|
||||
const oceanOpenApi = read('../../firstleg/src/main/resources/openapi/ocean-bookings-v1.yaml')
|
||||
const overviewAcceptance = read('phase3-container-overview.mjs')
|
||||
const oceanAcceptance = read('ocean-bookings.mjs')
|
||||
const adminRoot = '../../../apps/admin-web/src/modules/'
|
||||
const releaseView = read(`${adminRoot}ocean-bookings/pages/OceanBookingDetailView.vue`)
|
||||
const releaseTypes = read(`${adminRoot}ocean-bookings/types.ts`)
|
||||
const truckApi = read(`${adminRoot}trucking/api/platformTrucking.ts`)
|
||||
const truckAttachmentDialog = read(`${adminRoot}trucking/components/TruckAttachmentDialog.vue`)
|
||||
const truckList = read(`${adminRoot}trucking/pages/TruckAppointmentsView.vue`)
|
||||
const truckForm = read(`${adminRoot}trucking/components/TruckAppointmentForm.vue`)
|
||||
const containerList = read(`${adminRoot}container-loading/pages/ContainersView.vue`)
|
||||
const containerLoadingAcceptance = read('phase3-container-loading.mjs')
|
||||
const truckAcceptance = read('truck-appointments.mjs')
|
||||
const matrix = read('../../../docs/三期/货柜管理全链验收矩阵-2026-08-13.md')
|
||||
|
||||
test('release request freezes all five transport fields and UI blocks incomplete submission', () => {
|
||||
assert.match(oceanOpenApi, /required: \[expectedVersion, billOfLadingNo, vesselName, voyageNo, estimatedSailingAt, estimatedArrivalAt\]/)
|
||||
for (const field of ['billOfLadingNo', 'vesselName', 'voyageNo', 'estimatedSailingAt', 'estimatedArrivalAt']) {
|
||||
assert.match(releaseTypes, new RegExp(`${field}: string`), field)
|
||||
assert.match(releaseView, new RegExp(`releaseForm\\.${field}`), field)
|
||||
assert.match(oceanAcceptance, new RegExp(`${field}:`), field)
|
||||
}
|
||||
assert.match(releaseView, /if \(!billOfLadingNo \|\| !vesselName \|\| !voyageNo \|\| !releaseForm\.estimatedSailingAt \|\| !releaseForm\.estimatedArrivalAt\)/)
|
||||
})
|
||||
|
||||
test('overview regression requires workflowStatus and distinguishes 100 percent awaiting review', () => {
|
||||
assert.match(overviewAcceptance, /'workflowStatus'/)
|
||||
assert.match(overviewAcceptance, /workflowStatus === 'AWAITING_REVIEW'/)
|
||||
assert.match(overviewOpenApi, /workflowStatus:/)
|
||||
assert.doesNotMatch(matrix, /PDA overview `workflowStatus` 缺失[\s\S]*BLOCKED_CONTRACT/)
|
||||
})
|
||||
|
||||
test('every overview photo uses its server-provided controlled downloadPath', () => {
|
||||
assert.match(overviewOpenApi, /\/api\/platform\/v1\/containers\/\{id\}\/pda\/photos\/\{fileId\}\/download/)
|
||||
assert.match(overviewAcceptance, /context\.requestBinary\('GET', downloadPath\)/)
|
||||
assert.equal(controlledPhotoDownload('/api/platform/v1/files/1', {
|
||||
http: 200, bytes: new Uint8Array([1]), contentType: 'image/png',
|
||||
}), true)
|
||||
for (const [path, response] of [
|
||||
['https://example.test/file', { http: 200, bytes: new Uint8Array([1]), contentType: 'image/png' }],
|
||||
['/api/platform/v1/files/1', { http: 200, bytes: new Uint8Array([1]), contentType: 'application/json' }],
|
||||
['/api/platform/v1/files/1', { http: 404, bytes: new Uint8Array(), contentType: 'image/png' }],
|
||||
]) assert.equal(controlledPhotoDownload(path, response), false)
|
||||
})
|
||||
|
||||
test('truck attachment API is frozen while Admin add-reference entry remains blocked', () => {
|
||||
assert.match(truckApi, /referenceTruckAppointmentAttachment/)
|
||||
assert.match(truckApi, /deleteTruckAppointmentAttachment/)
|
||||
assert.doesNotMatch(truckAttachmentDialog, /referenceTruckAppointmentAttachment/)
|
||||
assert.match(truckAcceptance, /删除后回查拖车预约附件列表/)
|
||||
assert.match(truckAcceptance, /!remainingAttachments\.some/)
|
||||
assert.match(matrix, /拖车附件新增入口缺失[\s\S]*BLOCKED_CONTRACT/)
|
||||
})
|
||||
|
||||
test('existing container number replacement keeps container plan and PDA identities stable', () => {
|
||||
assert.match(containerLoadingAcceptance, /replacementContainerNo/)
|
||||
assert.match(containerLoadingAcceptance, /containerBookingId: containerId/)
|
||||
assert.match(containerLoadingAcceptance, /loadingPlanId: planId/)
|
||||
assert.match(containerLoadingAcceptance, /pdaTaskId: taskId/)
|
||||
assert.match(containerLoadingAcceptance, /pdaIdentityStable: true/)
|
||||
})
|
||||
|
||||
test('customer public display uses tenantName without a legalName fallback', () => {
|
||||
for (const source of [truckList, containerList]) {
|
||||
assert.match(source, /label: customer\.tenantName/)
|
||||
}
|
||||
assert.match(truckForm, /return customer\.tenantName/)
|
||||
for (const source of [truckList, containerList, truckForm])
|
||||
assert.doesNotMatch(source, /customer\.legalName\?\.trim\(\) \|\| customer\.tenantName/)
|
||||
assert.match(matrix, /客户公开名称一致性[\s\S]*已统一 `tenantName`[\s\S]*`LANDED`/)
|
||||
})
|
||||
|
||||
test('Chinese status dialogs and 390px layout remain explicit Admin contract blockers', () => {
|
||||
assert.match(matrix, /中文状态、弹框与 390px 布局[\s\S]*BLOCKED_CONTRACT/)
|
||||
})
|
||||
@@ -136,7 +136,15 @@ export async function run(context) {
|
||||
context.assert(MODULE, '附件列表包含本轮上传引用', attachments, Array.isArray(attachmentList) && attachmentList.some((item) => id(item.id) === attachmentId), `attachments=${attachmentList?.length ?? 'n/a'}`, OPTIONS)
|
||||
const forged = await request(context, 'POST', `${path}/attachments`, { fileId: '999999999999999999', attachmentType: 'ACCEPTANCE_EVIDENCE', fileName: `${context.marker}-forged.txt`, fileHash: 'forged' })
|
||||
context.addStep(MODULE, '伪造附件 fileId 安全拒绝', forged, failure(forged), `HTTP=${forged.http}, code=${codeOf(forged)}`, OPTIONS)
|
||||
if (attachmentId) { const removed = await request(context, 'DELETE', `${path}/attachments/${attachmentId}`); context.apiSuccess(MODULE, '删除本轮拖车预约附件引用', removed, `attachmentId=${attachmentId}`, OPTIONS) }
|
||||
if (attachmentId) {
|
||||
const removed = await request(context, 'DELETE', `${path}/attachments/${attachmentId}`)
|
||||
context.apiSuccess(MODULE, '删除本轮拖车预约附件引用', removed, `attachmentId=${attachmentId}`, OPTIONS)
|
||||
const afterRemoval = await request(context, 'GET', `${path}/attachments`)
|
||||
const remainingAttachments = context.apiSuccess(MODULE, '删除后回查拖车预约附件列表', afterRemoval, '', OPTIONS)
|
||||
context.assert(MODULE, '删除后附件引用不再返回', afterRemoval,
|
||||
Array.isArray(remainingAttachments) && !remainingAttachments.some((item) => id(item.id) === attachmentId),
|
||||
`attachmentId=${attachmentId}, remaining=${remainingAttachments?.length ?? 'n/a'}`, OPTIONS)
|
||||
}
|
||||
|
||||
let version = Number(appointment.version)
|
||||
const edit = await request(context, 'PUT', path, { ...truckAppointmentPayload(context, suffix, bookingIdentity, version), vehicleNo: `粤C-${suffix.slice(-6)}` }); const edited = context.apiSuccess(MODULE, '编辑拖车预约', edit, `version=${version}`, OPTIONS); version = Number(edited?.version)
|
||||
|
||||
Reference in New Issue
Block a user