2026-08-13 16:14:54 +08:00
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import test from 'node:test'
2026-10-21 21:30:24 +08:00
import vm from 'node:vm'
2026-10-27 17:44:41 +08:00
import { normalizeFixedCustomerId , run } from './merchant-qualification-fixture.mjs'
2026-10-21 21:30:24 +08:00
import { AcceptanceRun } from '../admin-test-env-acceptance.mjs'
function fixedContext ( profileId = '2100000000000000001' ) {
const calls = []
return {
calls , marker : 'FIXED-CUSTOMER-TEST' , context : {}, createdResources : {},
options : { fixedCustomerId : 'TEN2100000000000000001' , merchantUsername : 'fixture-user' , merchantPassword : 'fixture-password' },
2026-10-23 04:48:02 +08:00
async request ( method , path , body ) {
calls . push ({ method , path , body })
2026-10-21 21:30:24 +08:00
const data = path . endsWith ( '/login' ) ? { accessToken : 'fixture-token' , refreshToken : 'fixture-refresh' }
: path . endsWith ( '/profile' ) ? { id : profileId , status : 'ACTIVE' , tenantName : 'Fixture' }
: { status : 'READY' }
return { http : 200 , body : { code : 200 , data } }
},
apiSuccess ( _module , _label , response ) { return response . body . data },
assert () {},
}
}
test ( 'fixed customer accepts business number without losing integer precision' , () => {
assert . equal ( normalizeFixedCustomerId ( 'TEN2100000000000000001' ), '2100000000000000001' )
assert . throws (() => normalizeFixedCustomerId ( 'TENinvalid' ))
})
test ( 'fixed customer preparation only authenticates and reads existing customer' , async () => {
const context = fixedContext ()
await run ( context , { publishEnvironment : false , requireDistinctCustomer : false })
assert . equal ( context . context . merchantQualificationFixture . reused , true )
assert . equal ( context . context . merchantQualificationFixture . tenantId , '2100000000000000001' )
assert . equal ( context . context . customerId , '2100000000000000001' )
assert . deepEqual ( context . calls . filter ( call => call . method !== 'GET' ). map ( call => call . path ), [ '/api/auth/login' , '/api/auth/logout' ])
assert . equal ( context . createdResources . merchantQualificationFixture . password , undefined )
})
test ( 'wrong customer identity stops preparation and logs out' , async () => {
const context = fixedContext ( '2100000000000000002' )
await assert . rejects ( run ( context , { publishEnvironment : false , requireDistinctCustomer : false }), /归属或状态不匹配/ )
assert . equal ( context . context . merchantQualificationFixture , undefined )
assert . equal ( context . calls . at ( - 1 ). path , '/api/auth/logout' )
assert . equal ( context . calls . length , 3 )
})
test ( 'second customer fixture cannot silently reuse the first customer' , async () => {
const context = fixedContext ()
await assert . rejects ( run ( context , { publishEnvironment : false }), /跨客户场景阻断/ )
assert . equal ( context . calls . length , 0 )
})
2026-10-23 04:48:02 +08:00
test ( 'second fixed customer validates its own credentials and profile without creating a customer or publishing primary credentials' , async () => {
const context = fixedContext ( '2100000000000000002' )
context . options . fixedSecondaryCustomer = { customerId : 'TEN2100000000000000002' , username : 'second-test' , password : 'second-password' }
const primaryOptions = structuredClone ( context . options )
const before = { username : process . env . MERCHANT_TEST_USERNAME , password : process . env . MERCHANT_TEST_PASSWORD }
await run ( context , { publishEnvironment : false })
assert . equal ( context . context . merchantQualificationFixture . tenantId , '2100000000000000002' )
assert . equal ( context . context . merchantQualificationFixture . username , 'second-test' )
assert . deepEqual ( context . calls [ 0 ]. body , { loginId : 'second-test' , password : 'second-password' })
assert . equal ( context . createdResources . merchantQualificationFixture . password , undefined )
assert . deepEqual ( context . options , primaryOptions )
assert . deepEqual ({ username : process . env . MERCHANT_TEST_USERNAME , password : process . env . MERCHANT_TEST_PASSWORD }, before )
assert . deepEqual ( context . calls . filter ( call => call . method !== 'GET' ). map ( call => call . path ), [ '/api/auth/login' , '/api/auth/logout' ])
})
for ( const fault of [ 'same customer' , 'wrong profile' , 'missing credentials' ]) test ( `second fixed customer rejects ${ fault } ` , async () => {
const context = fixedContext ()
context . options . fixedSecondaryCustomer = { customerId : fault === 'same customer' ? context . options . fixedCustomerId : '2100000000000000002' ,
username : 'second-test' , password : fault === 'missing credentials' ? '' : 'second-password' }
await assert . rejects ( run ( context , { publishEnvironment : false }), /第二个客户|第二个固定客户|归属或状态不匹配/ )
assert . ok ( context . calls . every ( call => call . method === 'GET' || [ '/api/auth/login' , '/api/auth/logout' ]. includes ( call . path )))
if ( fault !== 'wrong profile' ) assert . equal ( context . calls . length , 0 )
else assert . equal ( context . calls . at ( - 1 ). path , '/api/auth/logout' )
})
2026-10-27 17:44:41 +08:00
for ( const environment of [ 'test' , 'gray' ]) test ( ` ${ environment } fixture without fixed customer fails before any request` , async () => {
2026-10-21 21:30:24 +08:00
const context = fixedContext ()
2026-10-27 17:44:41 +08:00
context . options = { environment }
2026-10-21 21:30:24 +08:00
await assert . rejects ( run ( context ), /必须配置固定客户/ )
assert . equal ( context . calls . length , 0 )
})
2026-10-27 17:44:41 +08:00
test ( 'GRAY fixed customer reuses its credentials and does not publish them into TEST variables' , async () => {
const context = fixedContext (); context . options . environment = 'gray'
const before = Object . fromEntries ([ 'MERCHANT_TEST_USERNAME' , 'MERCHANT_TEST_PASSWORD' , 'MERCHANT_GRAY_USERNAME' , 'MERCHANT_GRAY_PASSWORD' ]. map ( key => [ key , process . env [ key ]]))
try {
await run ( context )
assert . equal ( context . createdResources . merchantQualificationFixture . reused , true )
assert . equal ( process . env . MERCHANT_TEST_USERNAME , before . MERCHANT_TEST_USERNAME )
assert . equal ( process . env . MERCHANT_TEST_PASSWORD , before . MERCHANT_TEST_PASSWORD )
assert . equal ( process . env . MERCHANT_GRAY_USERNAME , context . options . merchantUsername )
assert . equal ( context . calls . some ( call => call . path === '/api/platform/v1/iam/tenants' ), false )
} finally { for ( const [ key , value ] of Object . entries ( before )) { if ( value === undefined ) delete process . env [ key ]; else process . env [ key ] = value } }
})
2026-10-21 21:30:24 +08:00
test ( 'cross-customer financial helpers stop before provisioning or writing' , async () => {
for ( const [ file , name , end ] of [
[ 'finance-direct-paid-isolation.mjs' , 'createSecondCustomer' , 'async function verifyCustomerIsolation' ],
[ 'firstleg-container-fee-allocation.mjs' , 'provisionCrossCustomerPlan' , 'export async function run' ],
]) {
const source = readFileSync ( new URL ( file , import . meta . url ), 'utf8' )
const helper = source . slice ( source . indexOf ( `async function ${ name } (` ), source . indexOf ( end ))
const scope = vm . createContext ({})
vm . runInContext ( helper . replaceAll ( 'import.meta.url' , "'file:///unused-test-module.mjs'" ), scope )
await assert . rejects ( scope [ name ]({ options : { fixedCustomerId : '123' } }), /缺少第二个固定客户/ )
}
})
test ( 'fixed resource runner excludes lifecycle handlers without marking them passed' , async () => {
const runner = new AcceptanceRun ({ environment : 'test' , fixedCustomerId : '123' , quiet : true })
let calls = 0
2026-10-22 14:37:59 +08:00
for ( const name of [ 'customers' , 'supplemental-boundaries' , 'system' , 'master-data' , 'notifications' , 'pricing' ,
'phase4-overseas-master-data' ]) {
2026-10-21 21:30:24 +08:00
await runner . executeModule ( name , async () => { calls ++ })
}
assert . equal ( calls , 0 )
2026-10-22 14:37:59 +08:00
assert . equal ( runner . modules . length , 7 )
2026-10-21 21:30:24 +08:00
assert . ok ( runner . modules . every ( module => module . skipped && ! module . success ))
})
test ( 'both request entrypoints reject customer creation before network access' , async () => {
const runner = new AcceptanceRun ({ environment : 'test' })
for ( const method of [ 'request' , 'requestWithBody' ]) {
2026-10-22 11:56:47 +08:00
for ( const path of [ '/api/auth/register' , '/api/auth/register/?source=test' , '/api/platform/v1/iam/tenants' , '/api/platform/v1/iam/tenants/123/archive' , '/api/platform/v1/iam/tenants/123/submit-review' ]) {
2026-10-21 21:30:24 +08:00
await assert . rejects ( runner [ method ]( 'POST' , path , {}), /禁止创建、归档客户/ )
}
}
})
2026-08-13 16:14:54 +08:00
2026-10-23 04:48:02 +08:00
test ( 'report security rejects the secondary password even under an ordinary evidence field' , async () => {
for ( const leak of [ false , true ]) {
const password = 'secondary-private-test-credential'
const runner = new AcceptanceRun ({ environment : 'test' , fixedCustomerId : '123' , quiet : true , modules : [],
fixedSecondaryCustomer : { customerId : '456' , username : 'second-test' , password } })
runner . request = async () => ({ http : 200 , body : { code : 'A0401' } })
runner . createdResources . note = leak ? `unexpected detail: ${ password } ` : 'ordinary diagnostic evidence'
const report = await runner . run ()
assert . equal ( report . steps . find ( step => step . module === 'report-security' ). passed , ! leak )
}
})
2026-08-13 16:14:54 +08:00
const acceptance = readFileSync ( new URL ( '../admin-test-env-acceptance.mjs' , import . meta . url ), 'utf8' )
const fixture = readFileSync ( new URL ( './merchant-qualification-fixture.mjs' , import . meta . url ), 'utf8' )
2026-08-26 11:10:42 +08:00
const orders = readFileSync ( new URL ( './orders.mjs' , import . meta . url ), 'utf8' )
2026-08-13 16:14:54 +08:00
2026-10-27 17:44:41 +08:00
for ( const environment of [ undefined , 'dev' , 'prod' ]) test ( `unconfigured customer fixture never provisions even outside normal entry: ${ environment } ` , async () => {
const context = fixedContext (); context . options = { environment }
await assert . rejects ( run ( context ), /必须配置固定客户/ )
assert . equal ( context . calls . length , 0 )
const runner = new AcceptanceRun ({ environment })
for ( const entry of [ 'request' , 'requestWithBody' , 'requestMultipart' ]) {
for ( const path of [ '/api/platform/v1/iam/tenants/' , '/api/auth/register' , '/api/platform/v1/iam/tenants/123/archive' ])
await assert . rejects ( runner [ entry ]( 'POST' , path , new FormData ()), /固定客户模式禁止/ )
2026-09-30 12:58:15 +08:00
}
})
2026-08-13 16:14:54 +08:00
test ( '默认全量验收先准备已审核商户再运行订单链路' , () => {
2026-08-24 20:14:40 +08:00
const modules = acceptance . match ( /const CONFIGURED_MODULES = \[([^\n]+)\]/ ) ? .[ 1 ] || ''
2026-08-13 16:14:54 +08:00
assert . match ( modules , /^'merchant-qualification-fixture', 'customers'/ )
assert . ok ( modules . indexOf ( "'merchant-qualification-fixture'" ) < modules . indexOf ( "'orders'" ))
2026-08-24 20:14:40 +08:00
assert . match ( acceptance , /DEFAULT_MODULES\s*=\s*normalizeModuleOrder\(CONFIGURED_MODULES\)/ )
2026-08-13 16:14:54 +08:00
})
2026-10-27 17:44:41 +08:00
test ( '商户夹具只核验现有客户,不包含建客、上传资质和审核实现' , () => {
assert . match ( fixture , /dimension-initialization/ )
2026-08-25 17:17:32 +08:00
assert . match ( fixture , /status === 'READY'/ )
2026-10-27 17:44:41 +08:00
assert . match ( fixture , /environment === 'gray' \? 'MERCHANT_GRAY' : 'MERCHANT_TEST'/ )
for ( const removed of [ 'required-package' , 'submit-review' , '/approve' , 'administratorUsername' , 'temporaryPassword' ])
assert . ok ( ! fixture . includes ( removed ), removed )
2026-08-13 16:14:54 +08:00
assert . doesNotMatch ( fixture , /internal\/iam|UPDATE\s+iam_|INSERT\s+INTO\s+iam_/i )
})
2026-08-26 11:10:42 +08:00
test ( '批量待分柜参数固定同一客户并限制为订单测量链' , () => {
assert . match ( acceptance , /--customer-name <name>/ )
assert . match ( acceptance , /--merchant-username <name>/ )
assert . match ( acceptance , /--origin-warehouse-name <name>/ )
assert . match ( acceptance , /--channel-name <name>/ )
assert . match ( acceptance , /--wait-split-count <n>/ )
assert . match ( acceptance , /--order-profile-mode <mode>/ )
assert . match ( acceptance , /merchant-username和merchant-password必须同时提供/ )
assert . match ( acceptance , /modules\.splice\(fixtureIndex, 1\)/ )
assert . match ( acceptance , /wait-split-count必须是1-100的整数/ )
2026-09-04 15:19:45 +08:00
assert . match ( acceptance , /fixtureModules = new Set\(\['merchant-qualification-fixture', 'orders', 'inbound-measurement',[\s\S]{0,180}'coload-groups'[\s\S]{0,180}'firstleg-container-fee-allocation'/ )
assert . match ( acceptance , /批量WAIT_SPLIT只允许执行WAIT_SPLIT造数、同柜组与柜次跨客户分摊模块/ )
2026-08-26 11:10:42 +08:00
assert . match ( acceptance , /createAdditionalWaitSplitFixtures/ )
2026-08-26 17:09:02 +08:00
assert . match ( acceptance , /orderBlockingFailure[\s\S]*step\.deploymentBlocker/ )
assert . match ( acceptance , /measurementBlockingFailure[\s\S]*step\.deploymentBlocker/ )
assert . doesNotMatch ( acceptance , /this\.modules\.at\(-1\)\?\.success !== true/ )
2026-09-14 11:03:47 +08:00
assert . match ( acceptance , /measurement\.processStatus === expectedProcessStatus/ )
assert . match ( acceptance , /prepareDropshipManualCount \? 'WAIT_PALLETIZE' : 'WAIT_SPLIT'/ )
2026-08-26 11:10:42 +08:00
assert . match ( acceptance , /measurement\.finalStatus === 'APPROVED'/ )
assert . match ( acceptance , /waitSplitVariant\(sequence\)/ )
assert . match ( acceptance , /packageCountPerSku/ )
assert . match ( acceptance , /grossWeightPerPalletKg/ )
assert . match ( acceptance , /randomizedOrderProfile\(sequence\)/ )
assert . match ( acceptance , /\['PRIVATE', 'STANDARD'\]/ )
assert . match ( acceptance , /ONE_CLICK_DROPSHIP/ )
assert . match ( acceptance , /destinationWarehouseCode: order\.destinationWarehouseCode/ )
assert . match ( acceptance , /channelName: order\.channelName/ )
assert . match ( acceptance , /pricingTaxMode: order\.pricingTaxMode/ )
assert . match ( orders , /customerId: profile\.id/ )
assert . match ( orders , /customerName: companyName/ )
assert . match ( orders , /\['FBA', 'OVERSEAS', 'PRIVATE'\]\.includes/ )
assert . match ( orders , /destinationType === 'PRIVATE'/ )
assert . match ( orders , /PRIVATE_ADDRESS/ )
assert . match ( orders , /eligible\.length < 1/ )
2026-08-26 17:50:56 +08:00
assert . match ( orders , /Client 查询发货客户候选', partnerResponse, '', CONTRACT_OPTIONS/ )
2026-08-26 11:10:42 +08:00
assert . doesNotMatch ( orders , /customerName: master\.partner\?\.partnerName/ )
})