fix(auth): tighten refresh session revocation
This commit is contained in:
@@ -5,7 +5,7 @@ import userEvent from '@testing-library/user-event';
|
||||
import { useState } from 'react';
|
||||
import { beforeEach, expect, test, vi } from 'vitest';
|
||||
|
||||
import type { AuthUser } from '../../services/authService';
|
||||
import type { AuthSessionSummary, AuthUser } from '../../services/authService';
|
||||
import { LEGAL_CONSENT_STORAGE_KEY } from '../common/legalDocuments';
|
||||
import { AuthGate } from './AuthGate';
|
||||
import { useAuthUi } from './AuthUiContext';
|
||||
@@ -23,6 +23,10 @@ const authMocks = vi.hoisted(() => ({
|
||||
logoutAuthUser: vi.fn(),
|
||||
redeemRegistrationInviteCode: vi.fn(),
|
||||
resetPassword: vi.fn(),
|
||||
getAuthAuditLogs: vi.fn(),
|
||||
getAuthRiskBlocks: vi.fn(),
|
||||
getAuthSessions: vi.fn(),
|
||||
revokeAuthSessions: vi.fn(),
|
||||
sendPhoneLoginCode: vi.fn(),
|
||||
startWechatLogin: vi.fn(),
|
||||
consumeAuthCallbackResult: vi.fn(),
|
||||
@@ -42,11 +46,11 @@ vi.mock('../../services/authService', () => ({
|
||||
changePhoneNumber: vi.fn(),
|
||||
consumeAuthCallbackResult: authMocks.consumeAuthCallbackResult,
|
||||
getStoredLastLoginPhone: vi.fn(() => ''),
|
||||
getAuthAuditLogs: vi.fn(),
|
||||
getAuthAuditLogs: authMocks.getAuthAuditLogs,
|
||||
getAuthLoginOptions: authMocks.getAuthLoginOptions,
|
||||
getAuthRiskBlocks: vi.fn(),
|
||||
getAuthRiskBlocks: authMocks.getAuthRiskBlocks,
|
||||
getCurrentAuthUser: authMocks.getCurrentAuthUser,
|
||||
getAuthSessions: vi.fn(),
|
||||
getAuthSessions: authMocks.getAuthSessions,
|
||||
getCaptchaChallengeFromError: vi.fn(() => null),
|
||||
liftAuthRiskBlock: vi.fn(),
|
||||
loginWithPhoneCode: authMocks.loginWithPhoneCode,
|
||||
@@ -54,7 +58,7 @@ vi.mock('../../services/authService', () => ({
|
||||
logoutAuthUser: authMocks.logoutAuthUser,
|
||||
redeemRegistrationInviteCode: authMocks.redeemRegistrationInviteCode,
|
||||
resetPassword: authMocks.resetPassword,
|
||||
revokeAuthSession: vi.fn(),
|
||||
revokeAuthSessions: authMocks.revokeAuthSessions,
|
||||
sendPhoneLoginCode: authMocks.sendPhoneLoginCode,
|
||||
setStoredLastLoginPhone: vi.fn(),
|
||||
startWechatLogin: authMocks.startWechatLogin,
|
||||
@@ -73,9 +77,12 @@ vi.mock('../../hooks/useGameSettings', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('./AccountModal', () => ({
|
||||
AccountModal: () => null,
|
||||
}));
|
||||
vi.mock('./AccountModal', async () => {
|
||||
const actual =
|
||||
await vi.importActual<typeof import('./AccountModal')>('./AccountModal');
|
||||
|
||||
return actual;
|
||||
});
|
||||
|
||||
vi.mock('./BindPhoneScreen', () => ({
|
||||
BindPhoneScreen: () => <div>绑定手机号</div>,
|
||||
@@ -116,6 +123,10 @@ beforeEach(() => {
|
||||
authMocks.changePassword.mockResolvedValue(mockUser);
|
||||
authMocks.logoutAllAuthSessions.mockResolvedValue(undefined);
|
||||
authMocks.logoutAuthUser.mockResolvedValue(undefined);
|
||||
authMocks.getAuthAuditLogs.mockResolvedValue([]);
|
||||
authMocks.getAuthRiskBlocks.mockResolvedValue([]);
|
||||
authMocks.getAuthSessions.mockResolvedValue([]);
|
||||
authMocks.revokeAuthSessions.mockResolvedValue(undefined);
|
||||
authMocks.redeemRegistrationInviteCode.mockResolvedValue({
|
||||
center: {
|
||||
inviteCode: 'SY12345678',
|
||||
@@ -205,6 +216,21 @@ function LogoutStateProbe() {
|
||||
);
|
||||
}
|
||||
|
||||
function AccountPanelProbe() {
|
||||
const authUi = useAuthUi();
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
authUi?.openAccountModal();
|
||||
}}
|
||||
>
|
||||
打开账号面板
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
test('auth gate keeps platform content visible when phone login is available', async () => {
|
||||
authMocks.getAuthLoginOptions.mockResolvedValue({
|
||||
availableLoginMethods: ['phone'],
|
||||
@@ -786,3 +812,101 @@ test('auth gate separates sms and password login by tabs', async () => {
|
||||
expect(authMocks.authEntry).toHaveBeenCalledWith('13800000000', 'passw0rd');
|
||||
});
|
||||
});
|
||||
|
||||
test('auth gate revokes merged session group and refreshes sessions', async () => {
|
||||
const user = userEvent.setup();
|
||||
const initialSessions: AuthSessionSummary[] = [
|
||||
{
|
||||
sessionId: 'usess_remote',
|
||||
sessionIds: ['usess_remote', 'usess_remote_rotated'],
|
||||
sessionCount: 2,
|
||||
clientType: 'web_browser',
|
||||
clientRuntime: 'chrome',
|
||||
clientPlatform: 'windows',
|
||||
clientLabel: 'Windows / Chrome',
|
||||
deviceDisplayName: 'Windows / Chrome',
|
||||
miniProgramAppId: null,
|
||||
miniProgramEnv: null,
|
||||
userAgent: 'Mozilla/5.0',
|
||||
ipMasked: '203.0.*.*',
|
||||
isCurrent: false,
|
||||
createdAt: '2026-05-01T10:00:00.000Z',
|
||||
lastSeenAt: '2026-05-01T10:30:00.000Z',
|
||||
expiresAt: '2026-06-01T10:30:00.000Z',
|
||||
},
|
||||
];
|
||||
authMocks.getCurrentAuthUser.mockResolvedValue({
|
||||
user: mockUser,
|
||||
availableLoginMethods: ['phone'],
|
||||
});
|
||||
authMocks.getAuthSessions
|
||||
.mockResolvedValueOnce(initialSessions)
|
||||
.mockResolvedValueOnce([]);
|
||||
|
||||
render(
|
||||
<AuthGate>
|
||||
<AccountPanelProbe />
|
||||
</AuthGate>,
|
||||
);
|
||||
|
||||
await user.click(await screen.findByRole('button', { name: '打开账号面板' }));
|
||||
const accountDialog = await screen.findByRole('dialog', {
|
||||
name: '账号信息',
|
||||
});
|
||||
await user.click(within(accountDialog).getByRole('button', { name: '踢下线' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(authMocks.revokeAuthSessions).toHaveBeenCalledWith([
|
||||
'usess_remote',
|
||||
'usess_remote_rotated',
|
||||
]);
|
||||
expect(authMocks.getAuthSessions).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
test('auth gate clears account state after password change', async () => {
|
||||
const user = userEvent.setup();
|
||||
authMocks.getCurrentAuthUser.mockResolvedValue({
|
||||
user: mockUser,
|
||||
availableLoginMethods: ['phone'],
|
||||
});
|
||||
authMocks.getAuthSessions.mockResolvedValue([]);
|
||||
authMocks.changePassword.mockResolvedValue(mockUser);
|
||||
|
||||
render(
|
||||
<AuthGate>
|
||||
<div>
|
||||
<LogoutStateProbe />
|
||||
<AccountPanelProbe />
|
||||
</div>
|
||||
</AuthGate>,
|
||||
);
|
||||
|
||||
expect(await screen.findByText('当前用户:测试玩家')).toBeTruthy();
|
||||
await user.click(screen.getByRole('button', { name: '打开账号面板' }));
|
||||
|
||||
const accountDialog = await screen.findByRole('dialog', {
|
||||
name: '账号信息',
|
||||
});
|
||||
await user.click(
|
||||
within(accountDialog).getByRole('button', { name: '修改密码' }),
|
||||
);
|
||||
|
||||
const passwordDialog = await screen.findByRole('dialog', {
|
||||
name: '修改登录密码',
|
||||
});
|
||||
await user.type(within(passwordDialog).getByLabelText('当前密码'), 'oldpass1');
|
||||
await user.type(within(passwordDialog).getByLabelText('新密码'), 'newpass1');
|
||||
await user.click(
|
||||
within(passwordDialog).getByRole('button', { name: '确认修改密码' }),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(authMocks.changePassword).toHaveBeenCalledWith(
|
||||
'oldpass1',
|
||||
'newpass1',
|
||||
);
|
||||
expect(screen.getByText('当前用户:未登录')).toBeTruthy();
|
||||
});
|
||||
expect(screen.queryByRole('dialog', { name: '账号信息' })).toBeNull();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user