166 lines
10 KiB
JavaScript
166 lines
10 KiB
JavaScript
import assert from 'node:assert/strict'
|
||
import { readFileSync } from 'node:fs'
|
||
import test from 'node:test'
|
||
|
||
const loginSource = readFileSync(new URL('./LoginView.vue', import.meta.url), 'utf8')
|
||
const registerSource = readFileSync(new URL('./RegisterView.vue', import.meta.url), 'utf8')
|
||
const authShellSource = readFileSync(new URL('../components/AuthShell.vue', import.meta.url), 'utf8')
|
||
const legalSource = readFileSync(new URL('./LegalDocumentView.vue', import.meta.url), 'utf8')
|
||
const legalDocumentsSource = readFileSync(new URL('../legalDocuments.ts', import.meta.url), 'utf8')
|
||
const authRoutesSource = readFileSync(new URL('../routes.ts', import.meta.url), 'utf8')
|
||
const authStoreSource = readFileSync(new URL('../store/auth.ts', import.meta.url), 'utf8')
|
||
const httpSource = readFileSync(new URL('../../../api/http.ts', import.meta.url), 'utf8')
|
||
const sessionSource = readFileSync(new URL('../../../api/session.ts', import.meta.url), 'utf8')
|
||
const authApiSource = readFileSync(new URL('../api/auth.ts', import.meta.url), 'utf8')
|
||
const mainSource = readFileSync(new URL('../../../main.ts', import.meta.url), 'utf8')
|
||
const appSource = readFileSync(new URL('../../../App.vue', import.meta.url), 'utf8')
|
||
const routerSource = readFileSync(new URL('../../../router/index.ts', import.meta.url), 'utf8')
|
||
const stylesSource = readFileSync(new URL('../../../styles/index.css', import.meta.url), 'utf8')
|
||
|
||
test('uses one prototype-inspired responsive shell for login and registration', () => {
|
||
assert.match(loginSource, /<AuthShell/)
|
||
assert.match(registerSource, /<AuthShell/)
|
||
assert.match(authShellSource, /auth-ocean\.jpg/)
|
||
assert.match(authShellSource, /class="auth-visual"/)
|
||
assert.match(stylesSource, /\.auth-panel \{[^}]*grid-template-columns:/s)
|
||
assert.match(stylesSource, /@media \(max-width: 960px\)[\s\S]*?\.auth-visual \{ display: none; \}/)
|
||
assert.match(stylesSource, /@media \(max-width: 720px\)[\s\S]*?\.register-form-grid \{ grid-template-columns: 1fr; \}/)
|
||
})
|
||
|
||
test('groups the full registration contract without adding out-of-scope sign-in methods', () => {
|
||
for (const section of ['账号信息', '企业信息', '联系信息']) assert.match(registerSource, new RegExp(section))
|
||
assert.doesNotMatch(loginSource + registerSource, /微信登录|企业微信登录|短信验证码|邮箱验证码|lang-switcher/)
|
||
})
|
||
|
||
test('matches the frozen username-or-mobile login contract', () => {
|
||
assert.match(loginSource, /label="用户名或手机号"/)
|
||
assert.match(loginSource, /const usernamePattern = \/\^\[A-Za-z\]/)
|
||
assert.match(loginSource, /const mainlandMobilePattern = \/\^1\[3-9\]\\d\{9\}\$\//)
|
||
assert.match(authApiSource, /data: \{ loginId, password \}/)
|
||
assert.doesNotMatch(authApiSource, /data: \{ username, password \}/)
|
||
assert.match(loginSource, /typeof route\.query\.account === 'string'/)
|
||
assert.doesNotMatch(loginSource, /developmentHint|demo-tip|merchant_admin|联调账号|本地后端/)
|
||
assert.match(loginSource, /form\.remember/)
|
||
assert.match(loginSource, /忘记密码/)
|
||
})
|
||
|
||
test('persists rotating tokens independently and only remembers the login id by choice', () => {
|
||
assert.match(sessionSource, /ACCESS_TOKEN_KEY/)
|
||
assert.match(sessionSource, /REFRESH_TOKEN_KEY/)
|
||
assert.match(sessionSource, /localStorage\.setItem\(ACCESS_TOKEN_KEY, tokens\.accessToken\)/)
|
||
assert.match(sessionSource, /localStorage\.setItem\(REFRESH_TOKEN_KEY, tokens\.refreshToken\)/)
|
||
assert.match(sessionSource, /persistRememberedLoginId/)
|
||
assert.match(loginSource, /getRememberedLoginId/)
|
||
assert.match(loginSource, /记住账号/)
|
||
assert.match(sessionSource, /clearSessionTokens/)
|
||
})
|
||
|
||
test('implements every backend registration field and real submission', () => {
|
||
for (const field of ['username', 'companyName', 'displayName', 'legalPerson', 'phone', 'contactEmail', 'countryCode', 'address', 'password', 'agreement']) {
|
||
assert.match(registerSource, new RegExp(`prop="${field}"`))
|
||
}
|
||
assert.match(registerSource, /label="登录手机号" prop="phone"/)
|
||
assert.doesNotMatch(registerSource, /legalPhone/)
|
||
assert.match(authApiSource, /phone: string/)
|
||
assert.doesNotMatch(authApiSource, /legalPhone/)
|
||
assert.match(registerSource, /注册时同时初始化公司联系电话;注册后两者可分别修改,互不联动/)
|
||
assert.match(registerSource, /registerMerchant\(/)
|
||
assert.match(registerSource, /4–64 位/)
|
||
assert.match(registerSource, /8–72 位/)
|
||
assert.match(registerSource, /label="法定代表人(选填)" prop="legalPerson"/)
|
||
assert.doesNotMatch(registerSource, /label="法定代表人[^\"]*" prop="legalPerson" required/)
|
||
assert.doesNotMatch(registerSource, /required: true, message: '请输入法定代表人'/)
|
||
assert.match(authApiSource, /legalPerson\?: string/)
|
||
})
|
||
|
||
test('shows an explicit registration entry directly below the login button', () => {
|
||
assert.match(loginSource, /<el-button[^>]*class="auth-submit"[\s\S]*?<p class="login-register-entry">还没有企业账号?<RouterLink to="\/register">立即注册<\/RouterLink><\/p>/)
|
||
assert.match(stylesSource, /\.login-register-entry \{[^}]*text-align: center;/)
|
||
})
|
||
|
||
test('shows immediate login after registration and separates company review from account activation', () => {
|
||
assert.match(registerSource, /企业账号注册成功/)
|
||
assert.match(registerSource, /无需等待注册审核/)
|
||
assert.match(registerSource, /提交订单前还需完成企业审核/)
|
||
assert.match(registerSource, /首次登录需上传营业执照/)
|
||
assert.match(registerSource, /法人身份证、货代资质证书和银行开户许可证可选填/)
|
||
assert.match(registerSource, /法人身份证及其他资质可选填/)
|
||
assert.match(registerSource, /立即登录/)
|
||
})
|
||
|
||
test('opens complete service and privacy documents from registration', () => {
|
||
assert.match(registerSource, /to="\/legal\/terms" target="_blank"/)
|
||
assert.match(registerSource, /to="\/legal\/privacy" target="_blank"/)
|
||
assert.doesNotMatch(registerSource, /showDocument/)
|
||
assert.match(authRoutesSource, /path: '\/legal\/terms'/)
|
||
assert.match(authRoutesSource, /path: '\/legal\/privacy'/)
|
||
assert.match(legalSource, /document\.sections/)
|
||
assert.match(legalDocumentsSource, /跨境物流中的信息出境/)
|
||
assert.match(legalDocumentsSource, /违禁品/)
|
||
})
|
||
|
||
test('uses real auth endpoints and single-flight refresh handling', () => {
|
||
assert.match(authApiSource, /\/auth\/login/)
|
||
assert.match(authApiSource, /\/auth\/register/)
|
||
assert.match(authApiSource, /revokeCapturedSession\(accessToken, refreshToken\)/)
|
||
assert.match(httpSource, /\/auth\/logout/)
|
||
assert.match(httpSource, /refreshPromise/)
|
||
assert.match(httpSource, /\/auth\/refresh/)
|
||
assert.match(httpSource, /persistTokenPair\(tokens\)/)
|
||
assert.match(httpSource, /token && !isPublicAuthRequest\(config\.url\)/)
|
||
assert.match(httpSource, /request\._retriedAfterRefresh = true/)
|
||
assert.match(httpSource, /return await transport\.request\(request\)/)
|
||
assert.match(httpSource, /envelope\.code !== '200'/)
|
||
assert.match(httpSource, /redirectToLoginAfterSessionExpiry/)
|
||
assert.match(httpSource, /error instanceof CrossTabRefreshTimeoutError/)
|
||
assert.match(httpSource, /const expiredError = new ApiError\(error\.message, 'A0401', 401\)/)
|
||
assert.match(httpSource, /error instanceof CrossTabRefreshTimeoutError[\s\S]*window\.dispatchEvent\(new CustomEvent\('cc-client-session-expired'\)\)[\s\S]*redirectToLoginAfterSessionExpiry\(\)/)
|
||
assert.match(httpSource, /window\.location\.replace\(`\/login\?redirect=\$\{encodeURIComponent\(redirect\)\}`\)/)
|
||
assert.match(authStoreSource, /decodeMerchantToken/)
|
||
assert.match(authStoreSource, /MerchantTokenExpiredError && getRefreshToken\(\)/)
|
||
assert.match(authStoreSource, /const refreshedToken = await refreshAuthenticatedSession\(\)/)
|
||
assert.match(authStoreSource, /applyToken\(refreshedToken\)/)
|
||
assert.doesNotMatch(authStoreSource, /mock-client:/)
|
||
})
|
||
|
||
test('handles impersonation exchange as public authentication and exposes failures', () => {
|
||
assert.match(httpSource, /publicAuthPaths = \[[^\]]*'\/auth\/impersonations\/exchange'/)
|
||
assert.match(appSource, /impersonationOrigin/)
|
||
assert.match(appSource, /await auth\.acceptImpersonationCode\(message\.code\)/)
|
||
assert.match(appSource, /type: 'impersonation-complete'/)
|
||
assert.match(appSource, /type: 'impersonation-failed'/)
|
||
assert.doesNotMatch(authStoreSource, /message\.type !== 'impersonation-ticket'/)
|
||
assert.match(routerSource, /isImpersonationHandoff[\s\S]*!isImpersonationHandoff/)
|
||
assert.match(loginSource, /route\.query\.impersonation === 'failed'/)
|
||
assert.match(loginSource, /代登录失败或授权已失效,请返回管理后台重新发起/)
|
||
})
|
||
|
||
test('treats only body code 200 as success', () => {
|
||
assert.match(httpSource, /if \(envelope\.code !== '200'\)/)
|
||
assert.doesNotMatch(httpSource, /if \(envelope\.code !== '0'\)/)
|
||
})
|
||
|
||
test('shows the backend credential error on login', () => {
|
||
assert.match(httpSource, /new ApiError\(apiErrorMessage\(envelope\.message, fieldErrors\), envelope\.code/)
|
||
assert.match(loginSource, /ElMessage\.error\(error instanceof Error \? error\.message : '登录失败,请检查账号后重试'\)/)
|
||
assert.doesNotMatch(loginSource, /登录状态已失效,请重新登录/)
|
||
})
|
||
|
||
test('shows a forced re-login notice after login identifiers change', () => {
|
||
assert.match(loginSource, /route\.query\.reason === 'identifiers-updated'/)
|
||
assert.match(loginSource, /所有会话均已退出,请重新登录/)
|
||
})
|
||
|
||
test('keeps a valid local session during temporary context and refresh outages', () => {
|
||
assert.match(authStoreSource, /function isExpiredSessionError/)
|
||
assert.match(authStoreSource, /if \(isExpiredSessionError\(error\)\)/)
|
||
assert.match(authStoreSource, /if \(!isCurrent\(\)\) return\s+sessionRestored\.value = false/)
|
||
assert.match(httpSource, /if \(mappedError\.status === 401 \|\| mappedError\.code === 'A0401'\)/)
|
||
assert.doesNotMatch(httpSource, /catch \(refreshError\) \{\s+clearSessionTokens\(\)/)
|
||
})
|
||
|
||
test('loads styles for programmatic Element Plus messages and dialogs', () => {
|
||
assert.match(mainSource, /element-plus\/es\/components\/message\/style\/css/)
|
||
assert.match(mainSource, /element-plus\/es\/components\/message-box\/style\/css/)
|
||
})
|