import test from 'node:test' import assert from 'node:assert/strict' import { readFile } from 'node:fs/promises' import { resolve } from 'node:path' import { createRequire } from 'node:module' const require = createRequire(import.meta.url) const esbuild = createRequire(require.resolve('vite'))('esbuild') async function loadModule(entry, stubService = false) { const result = await esbuild.build({ entryPoints: [entry], bundle: true, write: false, format: 'cjs', platform: 'node', alias: { '@': resolve('src') }, plugins: stubService ? [ { name: 'stub-base-service', setup(build) { build.onResolve({ filter: /BaseService$/ }, () => ({ path: 'base-service', namespace: 'stub' })) build.onLoad({ filter: /.*/, namespace: 'stub' }, () => ({ contents: ` export class BaseService { static calls = []; static put(url, data, config) { this.calls.push({url, data, config}); return Promise.resolve({code:0, data:{}}); } static update(url, data) { return this.put(url, data); } static post(url, data, config) { this.calls.push({url, data, config}); return Promise.resolve({code:0, data:{}}); } }` })) } } ] : [] }) const module = { exports: {} } new Function('require', 'module', 'exports', result.outputFiles[0].text)( require, module, module.exports ) return module.exports } test('new login password accepts only 8-12 ASCII letters/digits with all three classes', async () => { const { getNewLoginPasswordError, createNewLoginPasswordRule } = await loadModule( 'src/utils/validation/loginPassword.ts' ) for (const password of ['Abc12345', 'Abc123456789']) assert.equal(getNewLoginPasswordError(password), undefined) for (const password of [ 'Abc1234', 'Abc123456789X', 'abcd1234', 'ABCD1234', 'AbcDefGh', 'Abc1234!', 'Abc1234_', 'Abc1234 ', ' Abc12345', 'Abc1234\n', 'Abc1234中', 'Abc123😀', 'Abc12345' ]) { assert.ok(getNewLoginPasswordError(password), `must reject ${JSON.stringify(password)}`) } const rule = createNewLoginPasswordRule() for (const value of ['', undefined, null]) rule.validator({}, value, (error) => assert.equal(error, undefined)) rule.validator({}, 'Abc1234!', (error) => assert.ok(error instanceof Error)) }) test('account password requests use the right field and omit unchanged optional password', async () => { const { AccountService } = await loadModule('src/api/modules/account.ts', true) AccountService.calls = [] await AccountService.updateAccountPassword(12, 'Abc12345') assert.deepEqual(AccountService.calls[0].data, { new_password: 'Abc12345' }) await AccountService.updateAccount(12, { username: 'operator', password: '' }) assert.deepEqual(AccountService.calls[1].data, { username: 'operator' }) await assert.rejects(AccountService.updateAccount(12, { password: 'Abc1234!' })) await assert.rejects(AccountService.updateAccountPassword(12, 'abcd1234')) assert.equal(AccountService.calls.length, 2) }) test('writeoff terminal failure blocks recovery but allows resubmission; unknown result stays recoverable', async () => { const module = await loadModule( 'src/views/finance/employee-collection/employeeCollectionDisplay.ts' ) assert.equal(module.canRecoverApplicationApproval({ status: 4, recoverable: true }), false) assert.equal(module.canResubmitApplication({ status: 4 }), true) assert.equal( module.canRecoverApplicationApproval({ status: 0, approval_submission_status: 4, recoverable: true }), true ) assert.equal(module.canRecoverApplicationApproval({ status: 0, recoverable: false }), false) assert.equal(module.getApplicationStatusTagType(4), 'danger') for (const [value, label] of [ [0, '待提交'], [1, '提交中'], [2, '已提交'], [3, '提交失败'], [4, '结果未知'] ]) { assert.equal(module.getApplicationSubmissionLabel({ approval_submission_status: value }), label) } }) test('refund submission failure uses resubmit, not recovery, without changing status 6 anomaly handling', async () => { const module = await loadModule('src/config/constants/refund.ts') assert.equal(module.canRecoverRefundApproval({ status: 7, approval_recoverable: true }), false) assert.equal(module.canResubmitRefund({ status: 7 }), true) assert.equal(module.canResubmitRefund({ status: 3 }), true) assert.equal( module.canRecoverRefundApproval({ status: 1, submission_status: 4, approval_recoverable: true }), true ) assert.equal(module.canResubmitRefund({ status: 6, anomaly_flag: 1 }), false) assert.equal(module.canResubmitRefund({ status: 6, anomaly_flag: 0 }), true) assert.equal(module.RefundStatusNameMap[7], '审批提交失败') }) test('admin agent registration does not recover terminal failures and provides status fallback', async () => { const module = await loadModule('src/types/api/agentDistributionRegistration.ts') assert.equal(module.canRecoverAgentRegistration({ status: 3, recoverable: true }), false) assert.equal(module.canRecoverAgentRegistration({ status: 0, recoverable: true }), true) assert.equal(module.getAgentRegistrationStatusName({ status: 3 }), '审批提交失败') }) test('offline recharge failure keeps read actions and starts a new order only with existing role/permission', async () => { const { buildAgentRechargeActions } = await loadModule( 'src/views/finance/agent-recharge/agentRechargeActions.ts' ) const row = { id: 8, status: 7, payment_method: 'offline', payment_voucher_key: ['voucher'] } let restarted = false const options = { hasAuth: () => true, canCreateOffline: true, onRestart: () => { restarted = true }, onViewPaymentVoucher() {}, onConfirmPayment() {}, onReject() {}, onTriggerApproval() {} } const actions = buildAgentRechargeActions(row, options) assert.deepEqual( actions.map((action) => action.label), ['支付凭证', '重新发起'] ) actions[1].handler() assert.equal(restarted, true) assert.deepEqual( buildAgentRechargeActions(row, { ...options, canCreateOffline: false }).map( (action) => action.label ), ['支付凭证'] ) assert.deepEqual(buildAgentRechargeActions(row, { ...options, hasAuth: () => false }), []) }) test('withdrawal failure is independently typed from commission clawback and supports resubmit', async () => { const module = await loadModule('src/config/constants/commission.ts') assert.equal(module.WithdrawalStatusMap[5].label, '审批提交失败') assert.equal(module.canResubmitWithdrawal(5), true) assert.equal(module.canResubmitWithdrawal(3), true) assert.equal(module.canResubmitWithdrawal(1), false) assert.equal(module.getCommissionStatusLabel(5), '回溯') assert.equal( module.WITHDRAWAL_QUALIFICATION_STATUS_OPTIONS.find((item) => item.value === 4).label, '审批提交失败' ) }) test('order payment record status is independent from order status and channels preserve unknown codes', async () => { const module = await loadModule('src/utils/business/orderPayment.ts') assert.equal(module.getOrderPaymentRecordStatusName(0), '待支付') assert.equal(module.getOrderPaymentRecordStatusName(1), '已支付') assert.equal(module.getOrderPaymentRecordStatusName(2), '已失败') assert.equal(module.getOrderPaymentRecordStatusName(3), '已退款') assert.equal(module.getOrderPaymentRecordTagType(2), 'danger') assert.equal(module.getOrderPaymentChannelName('wechat'), '微信直连 v3') assert.equal(module.getOrderPaymentChannelName('wechat_v2'), '微信直连 v2') assert.equal(module.getOrderPaymentChannelName('fuiou'), '富友') assert.equal(module.getOrderPaymentChannelName(''), '-') assert.equal(module.getOrderPaymentChannelName('future-channel'), 'future-channel') }) test('exchange phone conflicts support HTTP error and business response without inventing a missing number', async () => { const module = await loadModule('src/utils/business/exchangeError.ts') const failure = { code: 1202, data: { phone: '13800000000' } } assert.match(module.getExchangePhoneConflictMessage(failure), /13800000000/) assert.equal( module.getExchangePhoneConflictMessage({ response: { data: failure } }), module.getExchangePhoneConflictMessage(failure) ) assert.match(module.getExchangePhoneConflictMessage({ code: 1202 }), /请先到/) assert.doesNotMatch(module.getExchangePhoneConflictMessage({ code: 1202 }), /undefined|null/) assert.equal(module.getExchangePhoneConflictMessage({ code: 1206 }), undefined) assert.equal( module.getExchangeBusinessErrorMessage({ response: { data: { message: '迁移失败' } } }), '迁移失败' ) const { ExchangeService } = await loadModule('src/api/modules/exchange.ts', true) ExchangeService.calls = [] const config = { requestOptions: { errorMessageMode: 'none' } } await ExchangeService.completeExchange(9, config) assert.deepEqual(ExchangeService.calls[0], { url: '/api/admin/exchanges/9/complete', data: {}, config }) }) test('exchange association handoff carries only the new asset and its bound phone as strings', async () => { const { getExchangePhoneAssociationQuery } = await loadModule( 'src/utils/business/exchangeError.ts' ) const newAsset = { asset_identifier: '89860624590014594318', associated_phones: ['19232713115'] } assert.deepEqual( getExchangePhoneAssociationQuery({ code: 1202, data: { phone: '19232713115' } }, newAsset), { asset_identifier: '89860624590014594318', phone: '19232713115' } ) assert.deepEqual( getExchangePhoneAssociationQuery({ response: { data: { code: 1202 } } }, newAsset), { asset_identifier: '89860624590014594318', phone: '19232713115' } ) assert.equal(getExchangePhoneAssociationQuery({ code: 1206 }, newAsset), undefined) assert.equal( getExchangePhoneAssociationQuery({ code: 1202, data: { phone: '19232713115' } }, {}), undefined ) assert.equal( getExchangePhoneAssociationQuery( { code: 1202 }, { asset_identifier: '89860624590014594318', associated_phones: [] } ), undefined ) assert.deepEqual( getExchangePhoneAssociationQuery({ code: 1202, data: { phone: '13900000000' } }, newAsset), { asset_identifier: '89860624590014594318', phone: '13900000000' } ) }) test('association preselection excludes old assets, different phones and invalid historical rows', async () => { const { getPhoneAssetAssociationTarget, getMatchingPhoneAssetAssociations, getPhoneAssetUnbindResultName } = await loadModule('src/utils/business/phoneAssetAssociation.ts') const target = getPhoneAssetAssociationTarget({ asset_identifier: ' 89860624590014594318 ', phone: '19232713115' }) const rows = [ { id: 1, asset_identifier: target.asset_identifier, phone: target.phone, status: 1 }, { id: 2, asset_identifier: '89860625770070732445', phone: target.phone, status: 1 }, { id: 3, asset_identifier: target.asset_identifier, phone: '13900000000', status: 1 }, { id: 4, asset_identifier: target.asset_identifier, phone: target.phone, status: 0 } ] assert.deepEqual( getMatchingPhoneAssetAssociations(rows, target).map((row) => row.id), [1] ) assert.deepEqual(getMatchingPhoneAssetAssociations(rows, { ...target, phone: '' }), []) assert.deepEqual( getPhoneAssetAssociationTarget({ asset_identifier: ['a', 'b'], phone: ['19232713115'] }), { asset_identifier: '', phone: '' } ) assert.equal(getPhoneAssetUnbindResultName(true), '解绑成功') assert.equal(getPhoneAssetUnbindResultName(false), '解绑失败') }) test('association page watches handoff changes, clears stale selections and retains manual unbind confirmation', async () => { const page = await readFile( 'src/views/asset-management/phone-asset-association/index.vue', 'utf8' ) assert.match(page, /getPhoneAssetAssociationTarget\(route.query\)/) assert.match(page, /toggleRowSelection\(row, true\)/) assert.match(page, /requestId !== listRequestId/) assert.match(page, /getPhoneAssetUnbindResultName\(row.success\)/) assert.doesNotMatch(page, /prop="success"/) assert.match(page, /batchForm.confirmed = false/) assert.match(page, /batchFormRef.value\?\.validate/) const table = await readFile('src/components/core/tables/ArtTable.vue', 'utf8') assert.match(table, /clearSelection:.*clearSelection\(\)/) }) test('password validation errors accept backend message as well as msg', async () => { const { normalizeApiError } = await loadModule('src/utils/business/apiError.ts') for (const field of ['msg', 'message']) { assert.equal( normalizeApiError({ isAxiosError: true, response: { status: 400, data: { [field]: '新密码不合法' } } }).message, '新密码不合法' ) } }) test('traffic snapshots preserve zero, missing values and MB precision', async () => { const { formatTrafficAlertMb } = await loadModule('src/utils/business/trafficAlert.ts') assert.equal(formatTrafficAlertMb(0), '0.00') assert.equal(formatTrafficAlertMb(1234.567), '1234.57') for (const value of [undefined, null, NaN]) assert.equal(formatTrafficAlertMb(value), '-') }) test('CSV download has the new exact four-column header and preserves phone identifiers as text', async () => { const { SHOP_BUSINESS_OWNER_CSV_TEMPLATE } = await loadModule( 'src/utils/business/shopBusinessOwnerImport.ts' ) assert.equal(SHOP_BUSINESS_OWNER_CSV_TEMPLATE.charCodeAt(0), 0xfeff) const [header, instruction] = SHOP_BUSINESS_OWNER_CSV_TEMPLATE.slice(1).trimEnd().split('\r\n') assert.deepEqual(header.split(','), ['店铺编码', '操作类型', '平台登录账号', '备注']) assert.equal(instruction.split(',').length, 4) assert.match(instruction, /手机号/) assert.match(instruction, /清空留空/) assert.doesNotMatch(SHOP_BUSINESS_OWNER_CSV_TEMPLATE, /业务员登录账号/) }) test('affected view contracts preserve raw payment history, package quota and device template', async () => { const order = await readFile('src/views/order-management/order-list/detail.vue', 'utf8') assert.match(order, /detailData\.payments \|\| \[\]/) assert.doesNotMatch(order, /payments\.(filter|sort)/) const rules = await readFile('src/views/package-management/traffic-alert-rules/index.vue', 'utf8') assert.match(rules, /item\.real_data_mb/) assert.doesNotMatch(rules, /row\.real_data_mb|prop: 'real_data_mb'/) const devices = await readFile( 'src/views/asset-management/task-management/device-task/index.vue', 'utf8' ) assert.ok( /独立的 IMEI\s+列/.test(devices), 'import instructions must require the standalone IMEI column' ) assert.match(devices, /@\/template\/设备导入模板\.xlsx/) assert.match(devices, /failed_items/) })