213 lines
13 KiB
JavaScript
213 lines
13 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
import { readFileSync } from 'node:fs'
|
|
import test from 'node:test'
|
|
import vm from 'node:vm'
|
|
import { normalizeFixedCustomerId, run } from './merchant-qualification-fixture.mjs'
|
|
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' },
|
|
async request(method, path, body) {
|
|
calls.push({ method, path, body })
|
|
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)
|
|
})
|
|
|
|
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')
|
|
})
|
|
|
|
for (const environment of ['test', 'gray']) test(`${environment} fixture without fixed customer fails before any request`, async () => {
|
|
const context = fixedContext()
|
|
context.options = { environment }
|
|
await assert.rejects(run(context), /必须配置固定客户/)
|
|
assert.equal(context.calls.length, 0)
|
|
})
|
|
|
|
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 } }
|
|
})
|
|
|
|
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
|
|
for (const name of ['customers', 'supplemental-boundaries', 'system', 'master-data', 'notifications', 'pricing',
|
|
'phase4-overseas-master-data']) {
|
|
await runner.executeModule(name, async () => { calls++ })
|
|
}
|
|
assert.equal(calls, 0)
|
|
assert.equal(runner.modules.length, 7)
|
|
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']) {
|
|
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']) {
|
|
await assert.rejects(runner[method]('POST', path, {}), /禁止创建、归档客户/)
|
|
}
|
|
}
|
|
})
|
|
|
|
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)
|
|
}
|
|
})
|
|
|
|
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')
|
|
const orders = readFileSync(new URL('./orders.mjs', import.meta.url), 'utf8')
|
|
|
|
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()), /固定客户模式禁止/)
|
|
}
|
|
})
|
|
|
|
test('默认全量验收先准备已审核商户再运行订单链路', () => {
|
|
const modules = acceptance.match(/const CONFIGURED_MODULES = \[([^\n]+)\]/)?.[1] || ''
|
|
assert.match(modules, /^'merchant-qualification-fixture', 'customers'/)
|
|
assert.ok(modules.indexOf("'merchant-qualification-fixture'") < modules.indexOf("'orders'"))
|
|
assert.match(acceptance, /DEFAULT_MODULES\s*=\s*normalizeModuleOrder\(CONFIGURED_MODULES\)/)
|
|
})
|
|
|
|
test('商户夹具只核验现有客户,不包含建客、上传资质和审核实现', () => {
|
|
assert.match(fixture, /dimension-initialization/)
|
|
assert.match(fixture, /status === 'READY'/)
|
|
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)
|
|
assert.doesNotMatch(fixture, /internal\/iam|UPDATE\s+iam_|INSERT\s+INTO\s+iam_/i)
|
|
})
|
|
|
|
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的整数/)
|
|
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造数、同柜组与柜次跨客户分摊模块/)
|
|
assert.match(acceptance, /createAdditionalWaitSplitFixtures/)
|
|
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/)
|
|
assert.match(acceptance, /measurement\.processStatus === expectedProcessStatus/)
|
|
assert.match(acceptance, /prepareDropshipManualCount \? 'WAIT_PALLETIZE' : 'WAIT_SPLIT'/)
|
|
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/)
|
|
assert.match(orders, /Client 查询发货客户候选', partnerResponse, '', CONTRACT_OPTIONS/)
|
|
assert.doesNotMatch(orders, /customerName: master\.partner\?\.partnerName/)
|
|
})
|