This commit is contained in:
2026-04-10 15:37:02 +08:00
parent 161cd32277
commit f19e482c8f
233 changed files with 43987 additions and 5127 deletions

View File

@@ -5,15 +5,30 @@ const { requestJsonMock } = vi.hoisted(() => ({
}));
import {
ApiClientError,
clearStoredAccessToken,
clearStoredAutoAuthCredentials,
getStoredAccessToken,
getStoredAutoAuthCredentials,
setStoredAccessToken,
} from './apiClient';
import {
authEntryWithStoredCredentials,
bindWechatPhone,
changePhoneNumber,
consumeAuthCallbackResult,
createAutoAuthCredentials,
ensureAutoAuthUser,
getAuthAuditLogs,
getAuthRiskBlocks,
getAuthSessions,
getCaptchaChallengeFromError,
liftAuthRiskBlock,
loginWithPhoneCode,
logoutAllAuthSessions,
revokeAuthSession,
sendPhoneLoginCode,
startWechatLogin,
} from './authService';
function createMemoryStorage() {
@@ -68,12 +83,17 @@ describe('authService auto auth', () => {
user: {
id: 'user_1',
username: 'guest_abc123abc123',
displayName: 'guest_abc123abc123',
phoneNumberMasked: null,
loginMethod: 'password',
bindingStatus: 'active',
wechatBound: false,
},
});
const user = await authEntryWithStoredCredentials({
username: 'guest_abc123abc123',
password: 'auto_secret_password',
username: ' guest_abc123abc123 ',
password: ' auto_secret_password ',
});
expect(user.username).toBe('guest_abc123abc123');
@@ -82,6 +102,16 @@ describe('authService auto auth', () => {
username: 'guest_abc123abc123',
password: 'auto_secret_password',
});
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/auth/entry',
expect.objectContaining({
body: JSON.stringify({
username: 'guest_abc123abc123',
password: 'auto_secret_password',
}),
}),
'登录失败',
);
});
it('reuses stored auto credentials before generating a new account', async () => {
@@ -92,6 +122,11 @@ describe('authService auto auth', () => {
user: {
id: 'user_saved',
username: 'guest_saveduser01',
displayName: 'guest_saveduser01',
phoneNumberMasked: null,
loginMethod: 'password',
bindingStatus: 'active',
wechatBound: false,
},
});
@@ -110,4 +145,354 @@ describe('authService auto auth', () => {
'登录失败',
);
});
it('sends phone login code through the new auth endpoint', async () => {
requestJsonMock.mockResolvedValue({
ok: true,
cooldownSeconds: 60,
expiresInSeconds: 300,
providerRequestId: 'mock-request-id',
});
const result = await sendPhoneLoginCode(' 138 0013 8000 ');
expect(result.cooldownSeconds).toBe(60);
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/auth/phone/send-code',
expect.objectContaining({
body: JSON.stringify({
phone: '13800138000',
scene: 'login',
}),
}),
'发送验证码失败',
);
});
it('sends phone change code with the correct scene', async () => {
requestJsonMock.mockResolvedValue({
ok: true,
cooldownSeconds: 60,
expiresInSeconds: 300,
providerRequestId: 'mock-request-id',
});
await sendPhoneLoginCode('13900139000', 'change_phone');
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/auth/phone/send-code',
expect.objectContaining({
body: JSON.stringify({
phone: '13900139000',
scene: 'change_phone',
}),
}),
'发送验证码失败',
);
});
it('extracts captcha challenge details from api errors', () => {
expect(getCaptchaChallengeFromError(new Error('plain error'))).toBeNull();
const captchaError = new ApiClientError({
message: '需要完成人机校验',
status: 403,
code: 'CAPTCHA_REQUIRED',
details: {
captchaChallenge: {
challengeId: 'captcha_1',
promptText: '请输入图中的验证码后再获取短信验证码',
imageDataUrl: 'data:image/svg+xml;base64,abc',
expiresInSeconds: 180,
},
},
});
expect(getCaptchaChallengeFromError(captchaError)).toEqual({
challengeId: 'captcha_1',
promptText: '请输入图中的验证码后再获取短信验证码',
imageDataUrl: 'data:image/svg+xml;base64,abc',
expiresInSeconds: 180,
});
});
it('stores jwt after phone login', async () => {
requestJsonMock.mockResolvedValue({
token: 'phone-jwt-token',
user: {
id: 'user_phone',
username: '138****8000',
displayName: '138****8000',
phoneNumberMasked: '138****8000',
loginMethod: 'phone',
bindingStatus: 'active',
wechatBound: false,
},
});
const user = await loginWithPhoneCode('13800138000', '123456');
expect(user.username).toBe('138****8000');
expect(getStoredAccessToken()).toBe('phone-jwt-token');
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/auth/phone/login',
expect.objectContaining({
body: JSON.stringify({
phone: '13800138000',
code: '123456',
}),
}),
'登录失败',
);
});
it('binds wechat phone and stores jwt after activation', async () => {
requestJsonMock.mockResolvedValue({
token: 'wechat-bind-token',
user: {
id: 'user_wechat',
username: '138****8000',
displayName: '138****8000',
phoneNumberMasked: '138****8000',
loginMethod: 'wechat',
bindingStatus: 'active',
wechatBound: true,
},
});
const user = await bindWechatPhone('13800138000', '123456');
expect(user.wechatBound).toBe(true);
expect(getStoredAccessToken()).toBe('wechat-bind-token');
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/auth/wechat/bind-phone',
expect.objectContaining({
body: JSON.stringify({
phone: '13800138000',
code: '123456',
}),
}),
'绑定手机号失败',
);
});
it('changes phone number without replacing the stored access token', async () => {
setStoredAccessToken('active-token');
requestJsonMock.mockResolvedValue({
user: {
id: 'user_phone',
username: '139****9000',
displayName: '139****9000',
phoneNumberMasked: '139****9000',
loginMethod: 'phone',
bindingStatus: 'active',
wechatBound: false,
},
});
const user = await changePhoneNumber('13900139000', '123456');
expect(user.phoneNumberMasked).toBe('139****9000');
expect(getStoredAccessToken()).toBe('active-token');
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/auth/phone/change',
expect.objectContaining({
body: JSON.stringify({
phone: '13900139000',
code: '123456',
}),
}),
'更换手机号失败',
);
});
it('starts wechat login by navigating to backend authorization url', async () => {
const assignMock = vi.fn();
vi.stubGlobal('window', {
localStorage: createMemoryStorage(),
dispatchEvent: vi.fn(),
location: {
pathname: '/',
hash: '',
search: '',
assign: assignMock,
},
history: {
replaceState: vi.fn(),
},
});
requestJsonMock.mockResolvedValue({
authorizationUrl: '/api/auth/wechat/callback?mock_code=wx-user&state=state123',
});
await startWechatLogin();
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/auth/wechat/start?redirectPath=%2F',
expect.objectContaining({
method: 'GET',
}),
'微信登录暂不可用',
);
expect(assignMock).toHaveBeenCalledWith(
'/api/auth/wechat/callback?mock_code=wx-user&state=state123',
);
});
it('consumes auth callback hash and stores token', () => {
const replaceStateMock = vi.fn();
vi.stubGlobal('window', {
localStorage: createMemoryStorage(),
dispatchEvent: vi.fn(),
location: {
pathname: '/',
search: '',
hash: '#auth_provider=wechat&auth_token=wx-token&auth_binding_status=pending_bind_phone',
},
history: {
replaceState: replaceStateMock,
},
});
const result = consumeAuthCallbackResult();
expect(result).toEqual({
provider: 'wechat',
bindingStatus: 'pending_bind_phone',
error: null,
});
expect(getStoredAccessToken()).toBe('wx-token');
expect(replaceStateMock).toHaveBeenCalledWith(null, '', '/');
});
it('loads auth sessions from account center endpoint', async () => {
requestJsonMock.mockResolvedValue({
sessions: [
{
sessionId: 'usess_1',
clientType: 'browser',
clientLabel: '网页端浏览器',
userAgent: 'Mozilla/5.0',
ipMasked: '127.0.*.*',
isCurrent: true,
createdAt: '2026-04-09T10:00:00.000Z',
lastSeenAt: '2026-04-09T10:30:00.000Z',
expiresAt: '2026-05-09T10:30:00.000Z',
},
],
});
const sessions = await getAuthSessions();
expect(sessions).toHaveLength(1);
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/auth/sessions',
expect.objectContaining({
method: 'GET',
}),
'读取登录设备失败',
);
});
it('loads recent auth audit logs', async () => {
requestJsonMock.mockResolvedValue({
logs: [
{
id: 'audit_1',
eventType: 'phone_login',
title: '手机号登录',
detail: '使用手机号 138****8000 完成登录',
ipMasked: '127.0.*.*',
userAgent: 'Mozilla/5.0',
createdAt: '2026-04-09T10:30:00.000Z',
},
],
});
const logs = await getAuthAuditLogs();
expect(logs).toHaveLength(1);
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/auth/audit-logs',
expect.objectContaining({
method: 'GET',
}),
'读取账号操作记录失败',
);
});
it('loads current risk blocks', async () => {
requestJsonMock.mockResolvedValue({
blocks: [
{
scopeType: 'phone',
title: '手机号保护中',
detail: '该手机号因异常尝试已被临时保护,请约 30 分钟后再试',
expiresAt: '2026-04-09T11:00:00.000Z',
remainingSeconds: 1800,
},
],
});
const blocks = await getAuthRiskBlocks();
expect(blocks).toHaveLength(1);
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/auth/risk-blocks',
expect.objectContaining({
method: 'GET',
}),
'读取安全状态失败',
);
});
it('lifts a risk block by scope type', async () => {
requestJsonMock.mockResolvedValue({
ok: true,
});
await liftAuthRiskBlock('phone');
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/auth/risk-blocks/phone/lift',
expect.objectContaining({
method: 'POST',
}),
'解除保护失败',
);
});
it('revokes a remote auth session by id', async () => {
requestJsonMock.mockResolvedValue({
ok: true,
});
await revokeAuthSession('usess_123');
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/auth/sessions/usess_123/revoke',
expect.objectContaining({
method: 'POST',
}),
'移除登录设备失败',
);
});
it('clears local auth state after logout all sessions', async () => {
setStoredAccessToken('stale-token');
requestJsonMock.mockResolvedValue({
ok: true,
});
await logoutAllAuthSessions();
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/auth/logout-all',
expect.objectContaining({
method: 'POST',
}),
'退出全部设备失败',
);
expect(getStoredAccessToken()).toBe('');
});
});