129 lines
7.5 KiB
JavaScript
129 lines
7.5 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
import { readFileSync } from 'node:fs'
|
|
import test from 'node:test'
|
|
|
|
const read = (relativeUrl) => readFileSync(new URL(relativeUrl, import.meta.url), 'utf8')
|
|
|
|
const controller = read('../../firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformInboundMeasurementController.java')
|
|
const service = read('../../firstleg/src/main/java/com/baiyue/firstleg/service/InboundMeasurementService.java')
|
|
const warehouseClient = read('../../firstleg/src/main/java/com/baiyue/firstleg/client/WarehousePalletLocationClient.java')
|
|
const serviceTests = read('../../firstleg/src/test/java/com/baiyue/firstleg/service/InboundMeasurementServiceTest.java')
|
|
const openapi = read('../../firstleg/src/main/resources/openapi/firstleg-v1.yaml')
|
|
const adminApi = read('../../../apps/admin-web/src/modules/inbounds/weight/api/weightOcr.ts')
|
|
const adminPanel = read('../../../apps/admin-web/src/modules/inbounds/weight/components/WeightOcrPanel.vue')
|
|
const adminBehavior = read('../../../apps/admin-web/src/modules/inbounds/weight/components/WeightOcrPanel.behavior.test.ts')
|
|
|
|
function endpointBlock(source, marker, nextMarker) {
|
|
const start = source.indexOf(marker)
|
|
assert.notEqual(start, -1, `missing contract marker: ${marker}`)
|
|
const end = source.indexOf(nextMarker, start + marker.length)
|
|
return source.slice(start, end === -1 ? source.length : end)
|
|
}
|
|
|
|
test('backend Controller exposes a keyword-filtered bounded page instead of an unbounded array', () => {
|
|
const block = endpointBlock(controller, '@GetMapping("/weight-pallet-options")', '@BusinessAudit')
|
|
assert.match(block, /ApiResponse<PageResult<InboundMeasurementResponses\.PalletSnapshot>>/)
|
|
assert.doesNotMatch(block, /ApiResponse<List<InboundMeasurementResponses\.PalletSnapshot>>/)
|
|
assert.match(block, /\bpage\b/)
|
|
assert.match(block, /\bsize\b/)
|
|
assert.match(block, /\bkeyword\b/)
|
|
|
|
const boundedSize = [...block.matchAll(/@Max\((\d+)\)/g)].map((match) => Number(match[1]))
|
|
assert.ok(boundedSize.some((maximum) => maximum > 0 && maximum <= 200),
|
|
'size must have a finite Controller maximum no greater than 200')
|
|
})
|
|
|
|
test('Firstleg forwards the bound to Warehouse instead of loading the full warehouse and slicing in memory', () => {
|
|
const clientBlock = endpointBlock(warehouseClient, '@GetMapping("/pallets/snapshots")', '@PostMapping')
|
|
assert.match(clientBlock, /ApiResponse<PageResult<PalletSnapshot>>/)
|
|
assert.doesNotMatch(clientBlock, /ApiResponse<List<PalletSnapshot>>/)
|
|
for (const parameter of ['page', 'size', 'keyword']) assert.match(clientBlock, new RegExp(`"${parameter}"`))
|
|
|
|
const serviceBlock = endpointBlock(service,
|
|
'public PageResult<InboundMeasurementResponses.PalletSnapshot> weightPalletOptions(',
|
|
'@Transactional')
|
|
assert.match(serviceBlock, /loadPalletSnapshotPage\(inbound, page, size, keyword\)/)
|
|
assert.match(service, /palletSnapshots\([\s\S]{0,300}?page[\s\S]{0,80}?size[\s\S]{0,80}?keyword/)
|
|
assert.doesNotMatch(serviceBlock, /\.stream\(\)[\s\S]*?\.skip\(/)
|
|
})
|
|
|
|
test('OpenAPI publishes page, size, keyword and a finite maximum without an array data contract', () => {
|
|
const block = endpointBlock(openapi,
|
|
' /api/platform/v1/first-leg/inbounds/{inboundId}/weight-pallet-options:',
|
|
' /api/platform/v1/first-leg/inbounds/{inboundId}/weight-results:')
|
|
for (const parameter of ['page', 'size', 'keyword']) {
|
|
assert.match(block, new RegExp(`name: ${parameter}`))
|
|
}
|
|
const maxima = [...block.matchAll(/maximum:\s*(\d+)/g)].map((match) => Number(match[1]))
|
|
assert.ok(maxima.some((maximum) => maximum > 0 && maximum <= 200),
|
|
'OpenAPI size parameter must declare a finite maximum no greater than 200')
|
|
const dataLine = block.match(/data:\s*\{[^\n]+/)?.[0] || ''
|
|
assert.doesNotMatch(dataLine, /type:\s*array/)
|
|
assert.match(block, /(?:records|WeightPalletOptionPage|PageResult)/)
|
|
})
|
|
|
|
test('Admin API requests a bounded remote-search page and does not type the response as an array', () => {
|
|
const block = endpointBlock(adminApi, 'export async function getWeightPalletOptions', 'export async function uploadWeightImage')
|
|
assert.match(block, /\bpage\b/)
|
|
assert.match(block, /\bsize\b/)
|
|
assert.match(block, /\bkeyword\b/)
|
|
assert.doesNotMatch(block, /ApiEnvelope<WeightPalletOption\[\]>/)
|
|
assert.match(block, /(?:records|PageResult|PageResource|PageResponse)/)
|
|
})
|
|
|
|
test('Admin skips candidates without weight edit permission or an editable inbound state', () => {
|
|
const loader = endpointBlock(adminPanel, 'async function loadPalletOptions', 'function chooseFile')
|
|
const gate = endpointBlock(adminPanel, 'const canLoadPalletOptions', 'const virtualPalletOptions')
|
|
assert.match(gate, /canEdit\.value/)
|
|
assert.match(gate, /editable\.value/)
|
|
assert.match(loader, /!canLoadPalletOptions\.value/)
|
|
})
|
|
|
|
test('Admin behavior tests prove both permission and inbound-state request suppression', () => {
|
|
const cases = [...adminBehavior.matchAll(/it\((['"`])([\s\S]*?)\1,\s*async\s*\(\)\s*=>\s*\{([\s\S]*?)\n\s*\}\)/g)]
|
|
const suppressionCases = cases.filter(([, , title, body]) => /getWeightPalletOptions/.test(body)
|
|
&& /not\.toHaveBeenCalled/.test(body))
|
|
assert.ok(suppressionCases.some(([, , title]) => /edit|permission|权限/i.test(title)),
|
|
'missing behavior case: no weight:edit must not request pallet candidates')
|
|
assert.ok(suppressionCases.some(([, , title]) => /status|editable|状态/i.test(title)),
|
|
'missing behavior case: non-editable inbound status must not request pallet candidates')
|
|
})
|
|
|
|
test('Admin remote search is bounded and never accumulates every candidate', () => {
|
|
assert.match(adminPanel, /\bremote\b/)
|
|
assert.match(adminPanel, /:remote-method=/)
|
|
const sizes = [...adminPanel.matchAll(/(?:PALLET|pallet).{0,40}(?:PAGE_SIZE|pageSize)\s*=\s*(\d+)/g)]
|
|
.map((match) => Number(match[1]))
|
|
assert.ok(sizes.some((size) => size > 0 && size <= 200),
|
|
'Admin remote search page size must be finite and no greater than 200')
|
|
assert.doesNotMatch(adminPanel, /palletOptions\.value\.(?:push|unshift)\s*\(/)
|
|
assert.doesNotMatch(adminPanel, /palletOptions\.value\s*=\s*\[\s*\.\.\.palletOptions\.value/)
|
|
assert.match(adminBehavior, /(?:remote search|远程搜索|keyword).{0,220}(?:size|page)/is)
|
|
})
|
|
|
|
test('Admin uses a virtualized selector so a large response cannot create one DOM option per candidate', () => {
|
|
assert.match(adminPanel, /<el-select-v2\b/)
|
|
assert.doesNotMatch(adminPanel, /<el-option[\s\S]*?v-for="option in palletOptions"/)
|
|
})
|
|
|
|
test('historical pallet snapshot remains read-only visible when it is no longer eligible', () => {
|
|
const cards = endpointBlock(adminPanel, '<article v-for="(row, index) in rows"', '</article>')
|
|
assert.match(cards, /row\.palletCode/)
|
|
assert.match(cards, /row\.palletType/)
|
|
assert.match(cards, /row\.palletTareWeightKg/)
|
|
assert.match(adminBehavior, /(?:historical|history|历史|失去候选|no longer eligible).{0,220}(?:palletCode|托盘编码|TP-)/is)
|
|
})
|
|
|
|
test('backend detail reads the stored historical pallet snapshot without revalidating current eligibility', () => {
|
|
assert.match(serviceTests, /detailUsesStoredPalletSnapshotAfterCandidateBecomesUnavailable/)
|
|
assert.match(serviceTests, /verify\(warehousePallets, never\(\)\)\.palletSnapshot/)
|
|
assert.match(serviceTests, /verify\(warehousePallets, never\(\)\)\.palletSnapshots/)
|
|
})
|
|
|
|
test('backend evidence fails closed for Warehouse non-2xx and network errors', () => {
|
|
assert.match(serviceTests, /weightPalletOptionsFailsClosedOnWarehouseNon2xxResponse/)
|
|
assert.match(serviceTests, /weightPalletOptionsFailsClosedOnWarehouseNetworkError/)
|
|
assert.match(serviceTests, /FeignException/)
|
|
assert.match(serviceTests, /RetryableException/)
|
|
})
|