test(firstleg): cover booking-level truck lifecycle

This commit is contained in:
2026-09-01 16:36:41 +08:00
parent 9098d8dfdf
commit 0ba3df2fce
4 changed files with 247 additions and 196 deletions
@@ -2,136 +2,97 @@ import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import test from 'node:test'
import { activeCustomerIdentity, inspectOceanBookingIdentity, minimalPdfBytes, platformWarehouseIdentity, truckAppointmentPayload } from './truck-appointments.mjs'
import { activeCustomerIdentity, inspectOceanBookingIdentity, minimalPdfBytes,
platformWarehouseIdentity, truckAppointmentPayload } from './truck-appointments.mjs'
test('PDF upload fixtures contain real PDF magic bytes instead of MIME-only text', () => {
const bytes = minimalPdfBytes('RUN-2-DISPATCH_ORDER')
const content = Buffer.from(bytes).toString('ascii')
const source = readFileSync(new URL('truck-appointments.mjs', import.meta.url), 'utf8')
test('PDF fixtures contain actual PDF magic bytes', () => {
const content = Buffer.from(minimalPdfBytes('TRUCK')).toString('ascii')
assert.equal(content.slice(0, 5), '%PDF-')
assert.match(content, /%%EOF\n$/)
const source = readFileSync(new URL('truck-appointments.mjs', import.meta.url), 'utf8')
assert.match(source, /new Blob\(\[minimalPdfBytes\(`\$\{context\.marker\}-BILL_OF_LADING`\)\], \{ type: 'application\/pdf' \}\)/)
assert.match(source, /new Blob\(\[minimalPdfBytes\(`\$\{context\.marker\}-DISPATCH_ORDER`\)\], \{ type: 'application\/pdf' \}\)/)
assert.doesNotMatch(source, /new Blob\(\[\s*`\$\{context\.marker\}-(?:BILL_OF_LADING|DISPATCH_ORDER)`\s*\], \{ type: 'application\/pdf' \}\)/)
})
test('booking prerequisite resolves an ACTIVE platform customer without fixture state', () => {
test('platform customer and warehouse helpers remain bounded', () => {
assert.deepEqual(activeCustomerIdentity({ id: '21', tenantName: '现有客户' }),
{ customerId: '21', customerName: '现有客户' })
assert.deepEqual(activeCustomerIdentity({ tenantId: 22, customerName: '备用客户' }),
{ customerId: '22', customerName: '备用客户' })
assert.equal(activeCustomerIdentity({ id: '23' }), null)
const source = readFileSync(new URL('truck-appointments.mjs', import.meta.url), 'utf8')
assert.match(source, /\/api\/platform\/v1\/iam\/tenants\?page=1&size=100&status=ACTIVE/)
assert.match(source, /fixtureCustomerId[\s\S]*\|\| candidates\[0\]/)
assert.deepEqual(platformWarehouseIdentity({ warehouseId: '41' }), { warehouseId: '41', platformTenantId: '0' })
})
test('channel catalog accepts the formal array response shape', () => {
const source = readFileSync(new URL('truck-appointments.mjs', import.meta.url), 'utf8')
assert.match(source, /const channelRows = records\(data\(channelsResponse\)\); const channel = channelRows\[0\]/)
assert.doesNotMatch(source, /\(data\(channelsResponse\) \|\| \[\]\)\[0\]/)
})
test('ACTIVE domestic platform warehouses are scanned through bounded pagination', () => {
const source = readFileSync(new URL('truck-appointments.mjs', import.meta.url), 'utf8')
assert.match(source, /for \(let page = 1; page <= 100; page\+\+\)/)
assert.match(source, /warehouse-admin\?tenantId=0&warehouseType=DOMESTIC&status=ACTIVE&page=\$\{page\}&size=100/)
assert.match(source, /const total = Number\(warehousePage\?\.total \?\? warehouseRows\.length\)/)
assert.match(source, /page \* 100 >= total/)
assert.match(source, /完整分页平台仓并解析有效海运渠道/)
assert.doesNotMatch(source, /193000000000200001|193000000000200002/)
})
test('platform warehouse scope is fixed to zero without requiring response tenantId', () => {
assert.deepEqual(platformWarehouseIdentity({ warehouseId: '41', warehouseName: '宁波仓' }),
{ warehouseId: '41', platformTenantId: '0' })
assert.deepEqual(platformWarehouseIdentity({ id: 42, warehouseName: '义乌仓' }),
{ warehouseId: '42', platformTenantId: '0' })
const source = readFileSync(new URL('truck-appointments.mjs', import.meta.url), 'utf8')
assert.match(source, /platformWarehouseIdentity\(item\)[\s\S]*platformTenantId: '0'/)
assert.doesNotMatch(source, /platformWarehouseIdentity\(item\)[^{]*\{[^}]*tenantId/)
})
test('booking identity is marker scoped and contains no customer snapshot', () => {
const result = inspectOceanBookingIdentity('RUN-2', { id: '91', marker: 'RUN-2', warehouseId: '41' })
test('booking identity contains no customer or container snapshot', () => {
const result = inspectOceanBookingIdentity('RUN', { id: '91', marker: 'RUN', warehouseId: '41' })
assert.deepEqual(result.identity, { oceanBookingId: '91', warehouseId: '41' })
assert.equal(Object.hasOwn(result.identity, 'customerId'), false)
assert.equal(inspectOceanBookingIdentity('RUN-2', { id: '90', marker: 'RUN-1', warehouseId: '41' }).identity, null)
for (const field of ['customerId', 'containerBookingId', 'loadingPlanId', 'containerNo', 'cabinetNo']) {
assert.equal(Object.hasOwn(result.identity, field), false)
}
})
test('payload requires booking container and carrier but never emits customer fields', () => {
test('save payload is strictly booking level', () => {
const context = { createdResources: { truckSupplier: { supplierId: '501', supplierName: '正式拖车商' } } }
const payload = truckAppointmentPayload(context, '2', {
oceanBookingId: '91', containerBookingId: '81', loadingPlanId: '71', warehouseId: '41', billOfLadingNo: 'BL-1',
oceanBookingId: '91', warehouseId: '41', billOfLadingNo: 'BL-1',
})
assert.deepEqual({ oceanBookingId: payload.oceanBookingId, containerBookingId: payload.containerBookingId,
carrierId: payload.carrierId, carrierName: payload.carrierName },
{ oceanBookingId: '91', containerBookingId: '81', carrierId: '501', carrierName: '正式拖车商' })
assert.equal(Object.hasOwn(payload, 'customerId'), false)
assert.equal(Object.hasOwn(payload, 'customerName'), false)
assert.equal(Object.hasOwn(payload, 'receivableAmount'), false)
assert.equal(payload.oceanBookingId, '91')
assert.equal(payload.carrierId, '501')
for (const field of ['customerId', 'customerName', 'containerBookingId', 'loadingPlanId', 'containerNo', 'cabinetNo', 'containers']) {
assert.equal(Object.hasOwn(payload, field), false, field)
}
})
test('backend DTO entity mapper service and OpenAPI have no truck customer protocol', () => {
test('fixture creates and releases booking without creating or linking a container', () => {
for (const label of ['自包含创建 DRAFT 订舱', 'DRAFT订舱不进入拖车候选',
'上传当前有效 BILL_OF_LADING 文件', '放舱自包含订舱', 'RELEASED且资料有效的订舱进入候选']) {
assert.match(source, new RegExp(label))
}
assert.doesNotMatch(source, /\/api\/platform\/v1\/containers|ocean-booking-link|isoContainerNo|创建自包含模拟柜/)
})
test('HTTP lifecycle covers uniqueness idempotency attachments and every action', () => {
for (const label of ['创建后订舱立即退出候选', '同订舱第二张非终态拖车单冲突',
'重复创建未改变版本和状态', '上传DISPATCH_ORDER拖车附件', '查询拖车附件列表',
'编辑订舱级拖车为USD应付费用', '整单推进SUBMIT', 'SUBMIT同键同载荷重放',
'SUBMIT重放版本零漂移', 'SUBMIT同键异载荷A0409',
'COMPLETED后订舱恢复候选', 'COMPLETED后同订舱可新建下一张拖车单',
'CANCELLED后订舱恢复候选', 'CANCELLED后同订舱可再次新建',
'EXCEPTION后订舱恢复候选', 'EXCEPTION后同订舱可再次新建']) {
assert.match(source, new RegExp(label), label)
}
assert.match(source, /\['DISPATCH', '派车'\], \['PICKUP', '提柜'\], \['ARRIVE', '到仓'\]/)
assert.match(source, /\['LOAD', '装货'\], \['DEPART', '离仓'\], \['COMPLETE', '完成'\]/)
assert.match(source, /context\.apiSuccess\(MODULE, `整单推进\$\{name\}`/)
})
test('negative matrix covers booking warehouse bill carrier and currency', () => {
for (const label of ['错误订舱拒绝创建', '错误仓库拒绝创建', '错误提单拒绝创建',
'错误承运商拒绝创建', '非法拖车币种拒绝创建']) assert.match(source, new RegExp(label))
})
test('backend and OpenAPI expose no container write or response protocol', () => {
const paths = [
'../../firstleg/src/main/java/com/baiyue/firstleg/dto/TruckAppointmentDtos.java',
'../../firstleg/src/main/java/com/baiyue/firstleg/entity/TruckAppointmentEntity.java',
'../../firstleg/src/main/java/com/baiyue/firstleg/mapper/TruckAppointmentMapper.java',
'../../firstleg/src/main/java/com/baiyue/firstleg/service/TruckAppointmentService.java',
'../../firstleg/src/main/resources/openapi/truck-appointments-v1.yaml',
]
for (const path of paths) {
const source = readFileSync(new URL(path, import.meta.url), 'utf8')
assert.doesNotMatch(source, /customerId|customerName|customer_id|customer_name/)
}
})
test('backend freezes released booking real container bill file and carrier rules', () => {
const dto = readFileSync(new URL('../../firstleg/src/main/java/com/baiyue/firstleg/dto/TruckAppointmentDtos.java', import.meta.url), 'utf8')
const dto = readFileSync(new URL(paths[0], import.meta.url), 'utf8')
assert.match(dto, /@NotNull @Positive Long oceanBookingId/)
assert.match(dto, /@NotNull @Positive Long containerBookingId/)
assert.match(dto, /@NotNull @Positive Long carrierId/)
const service = readFileSync(new URL('../../firstleg/src/main/java/com/baiyue/firstleg/service/TruckAppointmentService.java', import.meta.url), 'utf8')
for (const token of ['"RELEASED"', 'containerBookingId未分配至该订舱', 'Iso6346ContainerNumber.require',
'oceanBookingDocuments.requireActiveValidDocument', 'suppliers.trucking', 'uk_truck_active_container']) {
assert.ok(service.includes(token), `missing ${token}`)
assert.doesNotMatch(dto, /SaveRequest\([^)]*(containerBookingId|loadingPlanId|containerNo|cabinetNo)/s)
const openApi = readFileSync(new URL(paths[3], import.meta.url), 'utf8')
for (const field of ['containerBookingId:', 'loadingPlanId:', 'containerNo:', 'cabinetNo:', 'containers:']) {
assert.doesNotMatch(openApi, new RegExp(field))
}
})
test('HTTP regression is self-contained and covers no-customer and idempotency matrix', () => {
const source = readFileSync(new URL('truck-appointments.mjs', import.meta.url), 'utf8')
for (const label of ['自包含创建 DRAFT 订舱', '上传当前有效 BILL_OF_LADING 文件', '放舱自包含订舱',
'绑定本轮真实 ISO 柜号', '拖车请求不含客户字段', '同柜第二条非终态预约拒绝且零漂移',
'同键同载荷重放', '同键重放版本零漂移', '同键异载荷拒绝', '错误订舱拒绝创建',
'错误柜号归属拒绝创建', '错误提单拒绝创建', '错误承运商拒绝创建', '非法拖车币种拒绝创建',
'RMB应付费用响应无客户及应收字段', '编辑为USD应付费用', 'USD应付费用保存且无应收字段',
'上传并关联DISPATCH_ORDER拖车附件', '拖车附件类型仅使用运营附件而非订舱提单',
'删除首次上传的拖车附件引用', '重新引用平台归属拖车附件', '附件列表包含重新引用的DISPATCH_ORDER',
'删除重新引用的拖车附件',
'派车命令返回状态身份与版本正确', '派车后回查拖车完整详情',
'详情字段完整且订舱柜号身份无客户字段漂移']) assert.match(source, new RegExp(label))
assert.match(source, /const detailResponse = await request\(context, 'GET', path\); const detail = data\(detailResponse\)/)
assert.match(source, /const detailGaps = truckAppointmentFieldGaps\(detail\)/)
assert.doesNotMatch(source, /truckAppointmentFieldGaps\(dispatched\)/)
assert.doesNotMatch(source, /phase3ContainerLoading|phase3Loading/)
assert.doesNotMatch(source, /receivableAmount\s*:/)
test('schema owns one active appointment per ocean booking', () => {
const sql = readFileSync(new URL('../../firstleg/src/main/resources/db/phase3-truck-appointment-schema.sql', import.meta.url), 'utf8')
assert.match(sql, /UNIQUE KEY uk_truck_active_ocean_booking\(active_ocean_booking_id\)/)
assert.match(sql, /DROP INDEX uk_truck_active_container/)
assert.match(sql, /visibility VARCHAR\(24\) NOT NULL DEFAULT 'PLATFORM'/)
})
test('admin entry keeps truck module independent of loading fixtures', () => {
const entry = readFileSync(new URL('../admin-test-env-acceptance.mjs', import.meta.url), 'utf8')
assert.match(entry, /\['truck-appointments', \['merchant-qualification-fixture'\]\]/)
assert.doesNotMatch(entry, /\['truck-appointments', \[[^\]]*phase3-loading/)
})
test('schema owns platform attachments and one active appointment per real container', () => {
const sql = readFileSync(new URL('../../firstleg/src/main/resources/db/phase3-truck-appointment-schema.sql', import.meta.url), 'utf8')
assert.doesNotMatch(sql, /customer_id|customer_name|idx_truck_appointment_customer/)
assert.match(sql, /UNIQUE KEY uk_truck_active_container\(active_container_booking_id\)/)
assert.match(sql, /visibility VARCHAR\(24\) NOT NULL DEFAULT 'PLATFORM'/)
})
test('method permissions remain independently enforced after removing customer scope', () => {
const controller = readFileSync(new URL('../../firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformTruckAppointmentController.java', import.meta.url), 'utf8')
for (const permission of ['platform:firstleg:truck-appointment:list', 'platform:firstleg:truck-appointment:create',
'platform:firstleg:truck-appointment:edit', 'platform:firstleg:truck-appointment:operate']) {
assert.match(controller, new RegExp(`hasAuthority\\('${permission}'\\)`))
}
})