Files
client-web/src/modules/auth/views/AuthViews.test.mjs
T

166 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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/)
})