110 lines
7.1 KiB
JavaScript
110 lines
7.1 KiB
JavaScript
import test from 'node:test'
|
|
import assert from 'node:assert/strict'
|
|
import { verifyFleetBookingReservation } from './fleet-booking-reservation-acceptance.mjs'
|
|
|
|
function fixture(fault = '') {
|
|
const baseline = [
|
|
{ fromIndex: 0, toIndex: 1, occupied: { palletSpaces: 2, weightKg: 200, volumeCbm: 2 }, remaining: { palletSpaces: 28, weightKg: 17800, volumeCbm: 88 } },
|
|
{ fromIndex: 1, toIndex: 2, occupied: { palletSpaces: 1, weightKg: 100, volumeCbm: 1 }, remaining: { palletSpaces: 29, weightKg: 17900, volumeCbm: 89 } },
|
|
]
|
|
const cargo = [
|
|
{ dispatchNo: 'D-TEST', customerId: '900719925474099301', warehousePalletId: '900719925474099311', palletNo: 'P-A', destinationKey: 'FBA:A' },
|
|
{ dispatchNo: 'D-TEST', customerId: '900719925474099301', warehousePalletId: '900719925474099312', palletNo: 'P-B', destinationKey: 'FBA:B' },
|
|
]
|
|
const tripProof = { tripNo: 'T-TEST', dispatchNo: 'D-TEST', vehicleId: 'V-TEST', driverId: 'DR-TEST',
|
|
stops: [{ name: '起点' }, { name: 'A', destinationKey: 'FBA:A' }, { name: 'B', destinationKey: 'FBA:B' }] }
|
|
let tripVersion = 10, planned = false, segments = structuredClone(baseline)
|
|
const bookings = [], bodies = new Map(), calls = [], assertions = []
|
|
const response = (data, rejected = false) => ({ http: 200, body: { code: rejected ? 'A0409' : '200', data,
|
|
message: fault === 'wrong-conflict' ? '版本已变化' : '第1段路线超过车辆托位、重量或容积限制' },
|
|
requestId: 'test-request', traceId: fault === 'missing-trace' ? null : 'test-trace' })
|
|
const result = (booking, replayed = false) => response({ tripRecordVersion: tripVersion, booking: structuredClone(booking), replayed })
|
|
const context = { marker: 'OFFLINE-CONTRACT', createdResources: { fleetTrips: [tripProof] },
|
|
assert(module, name, res, passed, evidence, options) { assertions.push({ name, passed, options }) },
|
|
async request(method, path, body) {
|
|
const suffix = path.replace('/api/platform/v1/last-leg/fleet/trips/T-TEST', '')
|
|
calls.push({ method, suffix, body })
|
|
if (method === 'GET' && suffix === '') return response({ trip: { status: 'PLANNED', vehicleId: tripProof.vehicleId, driverId: tripProof.driverId }, dispatches: [{ dispatchNo: 'D-TEST' }] })
|
|
if (method === 'GET' && suffix === '/loading') return response({ confirmedLoad: fault === 'fake-loaded' ? {} : null })
|
|
if (suffix === '/capacity-plan') {
|
|
if (method === 'POST') { planned = true; tripVersion++ }
|
|
return response({ recordVersion: tripVersion, cargo, manifestHash: 'a'.repeat(64), blockReason: null,
|
|
capacityPlan: planned ? { manifestHash: 'a'.repeat(64) } : null,
|
|
planValid: planned, availableSegments: structuredClone(segments) })
|
|
}
|
|
if (method === 'GET' && suffix === '/bookings') return response({ tripRecordVersion: tripVersion, records: structuredClone(bookings) })
|
|
if (method === 'POST' && suffix === '/bookings') {
|
|
if (bodies.has(body.idempotencyKey)) {
|
|
const old = bodies.get(body.idempotencyKey)
|
|
if (JSON.stringify(old.body) !== JSON.stringify(body)) return response(null, true)
|
|
return result(old.booking, true)
|
|
}
|
|
assert.equal(body.tripRecordVersion, tripVersion)
|
|
const booking = { bookingNo: `FB-${bookings.length + 1}`, tripNo: tripProof.tripNo, customerId: body.customerId,
|
|
cargo: body.cargo, status: 'DRAFT', recordVersion: 0 }
|
|
bookings.push(booking); bodies.set(body.idempotencyKey, { body, booking }); tripVersion++
|
|
return result(booking)
|
|
}
|
|
const match = suffix.match(/^\/bookings\/(FB-\d+)\/(confirm|cancel)$/)
|
|
assert.ok(match, `unexpected ${method} ${suffix}`)
|
|
const booking = bookings.find(row => row.bookingNo === match[1])
|
|
if (bodies.has(body.idempotencyKey)) {
|
|
if (fault === 'replay-reoccupies' && match[2] === 'confirm') segments[0].remaining.palletSpaces--
|
|
return result(booking, true)
|
|
}
|
|
assert.equal(body.tripRecordVersion, tripVersion)
|
|
assert.equal(body.recordVersion, booking.recordVersion)
|
|
if (match[2] === 'confirm') {
|
|
if (booking.cargo.palletSpaces > segments[0].remaining.palletSpaces) return response(null, true)
|
|
booking.status = 'CONFIRMED'; booking.agreedAmount = 25
|
|
for (const index of (fault === 'wrong-segment' ? [0, 1] : [0])) {
|
|
segments[index].occupied.palletSpaces++; segments[index].occupied.weightKg++
|
|
segments[index].occupied.volumeCbm += 0.01
|
|
segments[index].remaining.palletSpaces--; segments[index].remaining.weightKg--
|
|
segments[index].remaining.volumeCbm -= 0.01
|
|
}
|
|
} else {
|
|
if (booking.status === 'CONFIRMED' && fault !== 'cancel-does-not-release') segments = structuredClone(baseline)
|
|
booking.status = 'CANCELLED'
|
|
}
|
|
tripVersion++; booking.recordVersion++; bodies.set(body.idempotencyKey, { body, booking })
|
|
return result(booking)
|
|
},
|
|
}
|
|
const measurements = (actual, stops) => {
|
|
assert.deepEqual(actual, cargo); assert.deepEqual(stops, tripProof.stops)
|
|
return cargo.map((row, index) => ({ warehousePalletId: row.warehousePalletId, palletSpaces: 1, weightKg: 100, volumeCbm: 1, deliveryIndex: index + 1 }))
|
|
}
|
|
return { context, tripProof, measurements, calls, bookings, assertions }
|
|
}
|
|
|
|
test('reservation HTTP helper verifies actual segments, original requests and cancellation without receiving or charging', async () => {
|
|
const f = fixture()
|
|
const proof = await verifyFleetBookingReservation(f.context, 'fleet-test', f.tripProof, f.measurements)
|
|
assert.equal(proof.status, 'VERIFIED')
|
|
assert.equal(proof.bookings.length, 2)
|
|
assert.ok(f.bookings.every(row => row.status === 'CANCELLED'))
|
|
assert.deepEqual(proof.finalSegments, proof.initialSegments)
|
|
assert.ok(proof.requests.every(row => row.requestId && row.traceId))
|
|
assert.ok(f.assertions.every(row => row.passed && row.options.deploymentBlocker))
|
|
assert.ok(f.calls.every(row => !/incoming|receipt-confirmation|transfer-delivery|departure/.test(row.suffix)))
|
|
assert.equal(proof.createRequest.customerId, '900719925474099301')
|
|
await assert.rejects(() => verifyFleetBookingReservation(f.context, 'fleet-test', f.tripProof, f.measurements), /本轮新建/)
|
|
})
|
|
|
|
for (const fault of ['wrong-segment', 'replay-reoccupies', 'cancel-does-not-release', 'fake-loaded', 'missing-trace', 'wrong-conflict']) {
|
|
test(`reservation helper fails closed on ${fault} even when HTTP reports success`, async () => {
|
|
const f = fixture(fault)
|
|
await assert.rejects(() => verifyFleetBookingReservation(f.context, 'fleet-test', f.tripProof, f.measurements))
|
|
assert.equal(f.tripProof.reservation.status, 'CHECKING')
|
|
assert.ok(f.assertions.some(row => !row.passed && row.options.deploymentBlocker))
|
|
if (['wrong-segment', 'replay-reoccupies'].includes(fault)) assert.equal(f.calls.some(row => row.suffix.endsWith('/cancel')), false)
|
|
})
|
|
}
|
|
|
|
test('reservation helper rejects foreign fixture before making any request', async () => {
|
|
const f = fixture()
|
|
await assert.rejects(() => verifyFleetBookingReservation(f.context, 'fleet-test', { ...f.tripProof }, f.measurements), /本轮新建/)
|
|
assert.equal(f.calls.length, 0)
|
|
})
|