This commit is contained in:
2026-10-27 17:44:41 +08:00
parent 57cd211973
commit db0e72daeb
37 changed files with 701 additions and 671 deletions
@@ -5,7 +5,7 @@ import { fixtureContract } from './fleet-cross-customer-acceptance.mjs'
// Strict offline HTTP model executes the real qualification and prepareFleetTrip helpers too.
function model(fault = '', countPatch = {}) {
const copy = value => structuredClone(value), calls = [], customers = [], transfers = [], dispatches = []
const copy = value => structuredClone(value), calls = [], customers = [{ id: '100', username: 'existing-a', password: 'offline-a' }, { id: '101', username: 'existing-b', password: 'offline-b' }], transfers = [], dispatches = []
const options = { execute: true, warehouseId: '10', destinationWarehouseId: '20', pdaToken: 'caller-pda-secret' }
const trip = { trip: { tripNo: 'TRIP', recordVersion: '0', warehouseId: '10', status: 'PLANNED', maxPalletSpaces: 30,
vehiclePlate: 'TEST', driverName: 'TEST', driverPhone: '19900000000' }, dispatches: [] }
@@ -28,7 +28,7 @@ function model(fault = '', countPatch = {}) {
warehousePalletId: `PAL${n}`, palletNo: `PALLET${n}`, scheduledPackages: 1, pickedPackages: 0,
destination: { key: `FBA:${transfer.destinationWarehouseCode}` } }))
const parentFixture = { marker: 'PARENT', tenantId: 'PARENT-ID', password: 'parent-secret' }
const context = { marker: 'RUN-CROSS', token: 'admin-secret', options: { customerName: 'Parent customer' }, context: { merchantQualificationFixture: parentFixture },
const context = { marker: 'RUN-CROSS', token: 'admin-secret', options: { customerName: 'Parent customer', fixedCustomerId: '100', merchantUsername: 'existing-a', merchantPassword: 'offline-a', fixedSecondaryCustomer: { customerId: '101', username: 'existing-b', password: 'offline-b' }, fixedFleetResources: [{ vehicleId: '11', driverId: '12' }] }, context: { merchantQualificationFixture: parentFixture },
createdResources: { merchantQualificationFixture: { marker: 'PARENT' } },
assert(_module, label, _response, passed) { assert.ok(passed, label) },
apiSuccess(_module, label, result) { assert.equal(result.http, 200, label); assert.equal(result.body.code, '200', label); return result.body.data },
@@ -59,17 +59,7 @@ function model(fault = '', countPatch = {}) {
if (path.endsWith('/warehouse-admin/20')) return response(destination)
if (path.endsWith('/warehouse-admin/21')) return response({ ...destination,
warehouse: { ...destination.warehouse, id: '21', warehouseCode: 'FBA2', warehouseName: 'Second destination' } })
if (path === '/api/platform/v1/iam/tenants') {
const n = customers.length, id = fault === 'reused customer' ? '100' : String(100 + n)
const customer = { id, username: `user${n}`, password: `fresh-secret-${n}`, marker: body.internalRemark }
customers.push(customer)
return response({ tenant: { id }, administratorUsername: customer.username, temporaryPassword: customer.password })
}
if (path.endsWith('/submit-review') && path.includes('/iam/')) return response({ status: 'PENDING' })
if (path === '/api/platform/v1/iam/reviews') return response(customers.map(row => ({ id: `REV${row.id}`, tenantId: row.id })))
if (path.endsWith('/dimension-initialization')) return response({ status: 'READY' })
if (path.endsWith('/approve')) return response({})
if (path.endsWith('/qualifications')) return response(['BUSINESS_LICENSE', 'LEGAL_PERSON_ID'].map(qualificationType => ({ qualificationType, status: 'APPROVED' })))
if (path === '/api/auth/login') {
assert.equal(token, '')
const customer = customers.find(row => row.username === body.loginId); assert.equal(body.password, customer.password)
@@ -81,7 +71,7 @@ function model(fault = '', countPatch = {}) {
return response(null)
}
const merchant = customers.find(row => token === `merchant-${row.id}-secret`)
if (path === '/api/merchant/v1/iam/tenant/profile') return response({ id: fault === 'foreign login' ? 'OTHER' : merchant.id })
if (path === '/api/merchant/v1/iam/tenant/profile') return response({ id: fault === 'foreign login' ? 'OTHER' : fault === 'reused customer' ? '100' : merchant.id, status: 'ACTIVE', tenantName: 'Existing fixture customer' })
if (path === '/api/merchant/v1/last-leg/warehouse-transfers') {
assert.ok(merchant); assert.equal(body.inboundCode, undefined); assert.deepEqual(body.sourceLastlegOrderIds, [])
assert.equal(body.needRelabel, false); assert.equal(body.items.length, 1); assert.equal(body.items[0].packageCount, 1); assert.equal(body.items[0].quantity, 1)
@@ -164,9 +154,9 @@ function model(fault = '', countPatch = {}) {
.map(code => ({ name: code, destinationKey: `FBA:${code}` }))] })
dispatches.push(native); return response(native)
}
if (path.endsWith('/master-data/vehicles')) { trip.trip.vehiclePlate = body.plateNumber.toUpperCase(); return response({ id: 'VEHICLE' }) }
if (path.endsWith('/fleet/drivers')) return response({ id: 'DRIVER', recordVersion: '0' })
if (path.endsWith('/fleet/drivers/DRIVER/review')) return response({ reviewStatus: 'APPROVED' })
if (path.endsWith('/master-data/vehicles/11')) return response({ id: '11', plateNumber: 'TEST', status: 'IN_SERVICE', region: 'OVERSEAS', ownership: 'OWNED' })
if (path.endsWith('/fleet/drivers/12')) return response({ id: '12', active: true, reviewStatus: 'APPROVED', details: { ownership: 'OWNED' }, qualification: { canDispatch: true, reason: 'ELIGIBLE' } })
if (raw.startsWith('/api/platform/v1/last-leg/fleet/trips?')) return response({ records: trip.trip.vehicleId ? [trip.trip] : [], total: trip.trip.vehicleId ? 1 : 0, size: 200, page: 1 })
if (path === '/api/platform/v1/last-leg/fleet/trips') return response({ records: [trip.trip], total: 1, size: 200, page: 1 })
if (path === '/api/platform/v1/last-leg/fleet/trips/TRIP') return response(trip)
if (path === '/api/platform/v1/last-leg/dispatches/DS-ONE/items') return response({ dispatchNo: native.dispatchNo, recordVersion: native.recordVersion,
@@ -178,7 +168,7 @@ function model(fault = '', countPatch = {}) {
assert.deepEqual(body.driver.evidence.photoFileIds, ['902'])
assert.deepEqual(body.driver.evidence.fileIds, ['903'])
Object.assign(trip.trip, { vehicleId: body.vehicle.vehicleId, driverId: body.driver.driverId })
native.vehicleInfo = { vehicle: { mode: 'MASTER', vehicleId: 'VEHICLE', plate: trip.trip.vehiclePlate }, driver: { driverId: 'DRIVER' } }
native.vehicleInfo = { vehicle: { mode: 'MASTER', vehicleId: '11', plate: trip.trip.vehiclePlate }, driver: { driverId: '12' } }
native.recordVersion++
} else if (method === 'POST') {
assert.equal(body.recordVersion, native.recordVersion); native.recordVersion++
@@ -222,7 +212,7 @@ test('real helpers build independent current-run customers, receipts and one com
assert.deepEqual(m.context.createdResources.merchantQualificationFixture, { marker: 'PARENT' })
assert.equal(m.context.options.customerName, 'Parent customer')
assert.deepEqual({ username: process.env.MERCHANT_TEST_USERNAME, password: process.env.MERCHANT_TEST_PASSWORD }, oldEnvironment)
assert.notEqual(m.customers[0].marker, m.customers[1].marker)
assert.equal(m.calls.some(row => row.raw === '/api/platform/v1/iam/tenants' || row.raw === '/api/auth/register'), false)
const proof = m.context.createdResources.fleetCrossCustomerFixture
assert.equal(proof.status, 'READY'); assert.deepEqual(proof.sessionCleanup.map(row => row.status), ['LOGGED_OUT', 'LOGGED_OUT'])
assert.equal(m.trip.dispatches.length, 1); assert.equal(new Set(fixture.members.map(row => row.dispatchNo)).size, 1); assert.deepEqual(m.trip.trip.stops.map(row => row.destinationKey).filter(Boolean), ['FBA:FBA'])
@@ -246,6 +236,32 @@ test('real helpers build independent current-run customers, receipts and one com
for (const secret of ['caller-pda-secret', 'admin-secret', 'fresh-secret-', 'merchant-100-secret', 'refresh-100-secret']) assert.ok(!persisted.includes(secret))
})
for (const history of [false, true]) test(`GRAY preparation reuses two customers and preserves the parent slot: history=${history}`, async () => {
const m = model()
Object.assign(m.context.options, { environment: 'gray', fixedCustomerId: 'TEN100', merchantUsername: 'existing-a', merchantPassword: 'offline-a',
fixedSecondaryCustomer: { customerId: 'TEN101', username: 'existing-b', password: 'offline-b' }, fixedFleetResources: [] })
if (history) {
const read = m.options.readInventory
m.options.readInventory = async scope => {
const raw = await read(scope), balance = structuredClone(raw.balances[0]), position = structuredClone(raw.positions[0]), ledger = structuredClone(raw.ledger[0])
Object.assign(balance, { id: 'HIST-B', inboundCode: 'OLD-RECEIPT' })
Object.assign(position, { id: 'HIST-P', balanceId: 'HIST-B', palletId: 'HIST-PALLET', palletNo: 'HIST-PALLET' })
Object.assign(ledger, { id: 'HIST-L', balanceId: 'HIST-B', positionId: 'HIST-P', inboundCode: 'OLD-RECEIPT', businessOrderNo: 'OLD-RECEIPT' })
raw.balances.push(balance); raw.positions.push(position); raw.ledger.push(ledger)
return raw
}
}
await assert.rejects(createFleetCrossCustomerFixture(m.context, m.options), /固定车辆司机组合/)
const proof = m.context.createdResources.fleetCrossCustomerFixture
assert.deepEqual(proof.members.map(row => row.customerId), ['100', '101'])
assert.ok(proof.members.every(row => row.qualification.reused === true && row.receiptVerified === true))
assert.equal(m.customers.length, 2)
assert.equal(m.calls.some(row => row.raw === '/api/platform/v1/iam/tenants' || row.raw === '/api/auth/register'), false)
assert.equal(m.context.context.merchantQualificationFixture, m.parentFixture)
assert.deepEqual(proof.sessionCleanup.map(row => row.status), ['LOGGED_OUT', 'LOGGED_OUT'])
if (history) assert.ok(proof.members.every(row => row.receipt.raw.balances.length === 2))
})
for (const fault of ['disabled receiving', 'reused customer', 'foreign login', 'missing forecast carton', 'missing declared quantity', 'foreign forecast item', 'uncounted carton', 'wrong counted pallet', 'wrong counted sku', 'missing carton measurement', 'zero counted pallet', 'wrong measured pallet weight', 'foreign position', 'truncated ledger', 'wrong raw scope', 'wrong raw ledger position', 'wrong raw ledger balance', 'foreign candidate', 'missing second cargo', 'foreign delivery', 'logout failure']) {
test(`fail closed, retain partial resources and clean owned sessions: ${fault}`, async () => {
const m = model(fault)