fix: some
All checks were successful
构建并部署前端到测试环境 / deploy (push) Successful in 55s

This commit is contained in:
luo
2026-10-14 17:08:32 +08:00
parent 16effb69f3
commit 0d77a7d55b
23 changed files with 1113 additions and 89 deletions

View File

@@ -3,6 +3,9 @@ import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
import { resolve } from 'node:path'
import { createRequire } from 'node:module'
import ts from 'typescript'
import * as vue from 'vue'
import { compileScript, parse } from '@vue/compiler-sfc'
const require = createRequire(import.meta.url)
const esbuild = createRequire(require.resolve('vite'))('esbuild')
@@ -47,11 +50,21 @@ async function loadModule(entry, stubService = false) {
return module.exports
}
test('new login password accepts only 8-12 ASCII letters/digits with all three classes', async () => {
test('new login password allows special characters and requires at least three of four classes', async () => {
const { getNewLoginPasswordError, createNewLoginPasswordRule } = await loadModule(
'src/utils/validation/loginPassword.ts'
)
for (const password of ['Abc12345', 'Abc123456789'])
for (const password of [
'Abc12345',
'Abc123456789',
'Abc1234!',
'Abc1234_',
'ABC1234!',
'abc1234!',
'Abcdefg!',
'Ab1~`-=[]{}',
'Ab1;:\\|,<>/?'
])
assert.equal(getNewLoginPasswordError(password), undefined)
for (const password of [
'Abc1234',
@@ -59,8 +72,9 @@ test('new login password accepts only 8-12 ASCII letters/digits with all three c
'abcd1234',
'ABCD1234',
'AbcDefGh',
'Abc1234!',
'Abc1234_',
'ABCDEF!!',
'abcdef!!',
'123456!!',
'Abc1234 ',
' Abc12345',
'Abc1234\n',
@@ -73,7 +87,218 @@ test('new login password accepts only 8-12 ASCII letters/digits with all three c
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))
rule.validator({}, 'Abc1234!', (error) => assert.equal(error, undefined))
rule.validator({}, 'abcdef!!', (error) => assert.ok(error instanceof Error))
for (const symbol of Array.from('!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~')) {
assert.equal(getNewLoginPasswordError(`Ab12345${symbol}`), undefined)
}
})
test('admin and scan-registration new-password forms share validation, hints and the 12-character input limit', async () => {
const forms = [
{
path: 'src/components/core/layouts/art-header-bar/ChangePasswordDialog.vue',
models: ['formData.new_password', 'formData.confirm_password'],
fields: ['new_password', 'confirm_password'],
hint: "t('passwordChange.passwordRule')"
},
{
path: 'src/views/account-management/account/index.vue',
models: ['formData.password'],
fields: ['password']
},
{
path: 'src/views/common/account-list.vue',
models: ['accountForm.password'],
fields: ['password']
},
{
path: 'src/views/shop-management/list/index.vue',
models: ['formData.init_password'],
fields: ['init_password']
},
{
path: 'src/views/account-management/enterprise-customer/index.vue',
models: ['form.password', 'passwordForm.password'],
fields: ['password', 'password']
},
{
path: 'src/views/agent-registration/index.vue',
models: ['form.password'],
fields: ['password'],
hint: null
}
]
for (const { path, models, fields, hint = 'NEW_LOGIN_PASSWORD_HINT' } of forms) {
const source = await readFile(path, 'utf8')
assert.match(source, /from '@\/utils\/validation\/loginPassword'/, path)
const inputs = [...source.matchAll(/<ElInput\b[\s\S]*?\/>/g)].map(([input]) => input)
for (const model of models) {
// Exact v-model also rejects a silent .trim modifier on password inputs.
const matches = inputs.filter((input) => input.includes(`v-model="${model}"`))
assert.equal(matches.length, 1, `${path}: ${model}`)
assert.match(matches[0], /type="password"/, `${path}: ${model}`)
assert.match(matches[0], /maxlength="12"/, `${path}: ${model}`)
if (hint) {
assert.ok(matches[0].includes(`:placeholder="${hint}"`), `${path}: ${model} hint`)
} else {
assert.match(matches[0], /aria-describedby="registration-password-rule"/, path)
assert.match(source, /id="registration-password-rule"[\s\S]*?NEW_LOGIN_PASSWORD_HINT/, path)
}
}
const rules = [
...source.matchAll(
/\b(password|new_password|confirm_password|init_password):\s*\[([\s\S]*?)\n\s*\]/g
)
]
assert.deepEqual(rules.map(([, field]) => field).sort(), [...fields].sort(), path)
for (const [, field, rule] of rules) {
assert.match(rule, /required: true/, `${path}: ${field}`)
assert.match(rule, /createNewLoginPasswordRule\(/, `${path}: ${field}`)
assert.doesNotMatch(rule, /\b(?:min|max|pattern):/, `${path}: ${field} stale rule`)
}
}
})
test('scan registration validates password and locally handles errors without changing public endpoint, payload or SMS behavior', async () => {
const { AgentDistributionService } = await loadModule(
'src/api/modules/agentDistribution.ts',
true
)
AgentDistributionService.calls = []
const params = {
distribution_code: 'local-test',
phone: '13800000000',
code: '123456',
shop_name: '本地测试店铺',
shop_code: 'LOCAL_TEST',
username: 'local_test'
}
for (const password of ['Abc12345', 'Abc123456789', 'ABC1234!', 'abc1234!', 'Abcdefg!']) {
await AgentDistributionService.registerAgent({ ...params, password })
assert.deepEqual(AgentDistributionService.calls.at(-1), {
url: '/api/c/v1/agent-distribution-registrations',
data: { ...params, password },
config: { requestOptions: { withToken: false, errorMessageMode: 'none' } }
})
}
for (const password of ['', 'Abc1234', 'Abc123456789X', 'abcd1234', 'Abc1234 ', 'Abc1234中']) {
await assert.rejects(AgentDistributionService.registerAgent({ ...params, password }))
}
assert.equal(AgentDistributionService.calls.length, 5)
await AgentDistributionService.sendCode({ phone: params.phone, scene: 'bind_phone' })
assert.deepEqual(AgentDistributionService.calls.at(-1), {
url: '/api/c/v1/auth/send-code',
data: { phone: params.phone, scene: 'bind_phone' },
config: { requestOptions: { withToken: false } }
})
})
test('scan registration masks template errors in both business and HTTP responses and uses only local feedback', async () => {
const { getAgentRegistrationFailureMessage } = await loadModule(
'src/utils/business/agentRegistrationError.ts'
)
for (const code of [1050, 1001, '1050', '1001']) {
const failure = { code, data: null, msg: '企业微信审批模板无效或不可访问' }
for (const value of [
failure,
{ ...failure, data: { detail: 'internal template settings' } },
{ isAxiosError: true, response: { status: 400, data: failure } },
{ isAxiosError: true, response: { status: 500, data: failure } },
{ data: failure }
]) {
assert.equal(getAgentRegistrationFailureMessage(value), '注册失败, 请联系管理员')
}
}
assert.equal(getAgentRegistrationFailureMessage({ code: 1203, msg: '验证码错误' }), '验证码错误')
assert.equal(
getAgentRegistrationFailureMessage({ code: 1204, message: '分销码不可用' }),
'分销码不可用'
)
assert.equal(
getAgentRegistrationFailureMessage({ isAxiosError: true, code: 'ECONNABORTED' }),
'请求超时,结果可能未知,请刷新确认'
)
for (const value of [null, undefined, new Error('network')]) {
assert.equal(typeof getAgentRegistrationFailureMessage(value), 'string')
}
const page = await readFile('src/views/agent-registration/index.vue', 'utf8')
assert.match(page, /ElMessage.error\(getAgentRegistrationFailureMessage\(res\)\)/)
assert.match(
page,
/catch \(error\)\s*\{\s*ElMessage.error\(getAgentRegistrationFailureMessage\(error\)\)/
)
assert.doesNotMatch(page, /ElMessage.error\('分销码不可用'\)/)
assert.match(page, /if \(!valid \|\| submitting.value\) return/)
})
test('scan registration emits exactly one masked message per template failure and retains the form for retry', async () => {
const { getAgentRegistrationFailureMessage } = await loadModule(
'src/utils/business/agentRegistrationError.ts'
)
const passwordValidation = await loadModule('src/utils/validation/loginPassword.ts')
const { descriptor } = parse(await readFile('src/views/agent-registration/index.vue', 'utf8'))
const script = compileScript(descriptor, { id: 'scan-registration-error-test' }).content
const compiled = ts.transpileModule(script, {
compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS }
}).outputText
for (const code of [1050, 1001]) {
for (const rejected of [false, true]) {
const messages = []
const failure = { code, data: null, msg: '企业微信审批模板无效或不可访问' }
const imports = {
vue: { ...vue, onBeforeUnmount() {} },
'vue-router': { useRoute: () => ({ query: {} }) },
'element-plus': { ElMessage: { error: (message) => messages.push(message) } },
'@/api/modules/agentDistribution': {
AgentDistributionService: {
registerAgent: async () => {
if (rejected) throw { isAxiosError: true, response: { status: 400, data: failure } }
return failure
}
}
},
'@/utils/constants/regionData': { regionData: [] },
'@/utils/validation/loginPassword': passwordValidation,
'@/utils/business/agentRegistrationError': { getAgentRegistrationFailureMessage }
}
const module = { exports: {} }
new Function('require', 'module', 'exports', compiled)(
(name) => {
assert.ok(imports[name], `Unexpected dependency: ${name}`)
return imports[name]
},
module,
module.exports
)
const state = module.exports.default.setup({}, { expose() {} })
Object.assign(state.form, { distribution_code: 'local-test', password: 'Abc1234!' })
let submission
state.formRef.value = {
validate: (callback) => {
submission = callback(true)
}
}
state.handleSubmit()
state.handleSubmit()
await submission
assert.deepEqual(messages, ['注册失败, 请联系管理员'])
assert.equal(state.submitted.value, false)
assert.equal(state.submitting.value, false)
assert.equal(state.form.distribution_code, 'local-test')
assert.equal(state.form.password, 'Abc1234!')
}
}
})
test('scan registration password feedback uses the shared rule rather than treating spaces or Chinese as symbols', async () => {
const page = await readFile('src/views/agent-registration/index.vue', 'utf8')
assert.match(page, /return getNewLoginPasswordError\(pwd\) \? 1 : 3/)
assert.match(page, /密码符合规则/)
assert.match(page, /aria-live="polite"/)
assert.match(page, /password-form-item :deep\(\.el-form-item__error\)[\s\S]*?position: static/)
assert.doesNotMatch(page, /8-32|maxlength="32"/)
assert.match(page, /password: form.password,/)
})
test('account password requests use the right field and omit unchanged optional password', async () => {
@@ -83,9 +308,26 @@ test('account password requests use the right field and omit unchanged optional
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 AccountService.updateAccount(12, { password: 'Abc1234!' })
assert.deepEqual(AccountService.calls[2].data, { password: 'Abc1234!' })
await AccountService.updateAccountPassword(12, 'ABC1234!')
assert.deepEqual(AccountService.calls[3].data, { new_password: 'ABC1234!' })
await assert.rejects(AccountService.updateAccount(12, { password: 'abcdef!!' }))
await assert.rejects(AccountService.updateAccountPassword(12, 'abcd1234'))
assert.equal(AccountService.calls.length, 2)
assert.equal(AccountService.calls.length, 4)
})
test('change-password dialog is appended to body so its overlay covers the sidebar stacking context', async () => {
const dialog = await readFile(
'src/components/core/layouts/art-header-bar/ChangePasswordDialog.vue',
'utf8'
)
assert.match(dialog, /append-to-body/)
assert.match(dialog, /createNewLoginPasswordRule\(t\('passwordChange.passwordRule'\)\)/)
for (const language of ['zh', 'en']) {
const locale = JSON.parse(await readFile(`src/locales/langs/${language}.json`, 'utf8'))
assert.doesNotMatch(locale.passwordChange.passwordRule, /不允许特殊符号|no special characters/)
}
})
test('writeoff terminal failure blocks recovery but allows resubmission; unknown result stays recoverable', async () => {