fix(auth): tighten refresh session revocation

This commit is contained in:
2026-05-13 15:04:37 +08:00
parent b13870f71b
commit 4fecf9c975
36 changed files with 1664 additions and 170 deletions

View File

@@ -31,6 +31,8 @@ function renderAccountModal(overrides?: {
riskBlocks?: AuthRiskBlockSummary[];
sessions?: AuthSessionSummary[];
auditLogs?: AuthAuditLogEntry[];
onRevokeSession?: (session: AuthSessionSummary) => Promise<void>;
revokingSessionIds?: string[];
initialSection?:
| 'appearance'
| 'account'
@@ -63,7 +65,10 @@ function renderAccountModal(overrides?: {
onRefreshSessions={vi.fn().mockResolvedValue(undefined)}
onLogoutAll={vi.fn().mockResolvedValue(undefined)}
onRefreshAuditLogs={vi.fn().mockResolvedValue(undefined)}
onRevokeSession={vi.fn().mockResolvedValue(undefined)}
onRevokeSession={
overrides?.onRevokeSession ?? vi.fn().mockResolvedValue(undefined)
}
revokingSessionIds={overrides?.revokingSessionIds ?? []}
changePhoneCaptchaChallenge={null}
onSendChangePhoneCode={vi.fn().mockResolvedValue({
cooldownSeconds: 60,
@@ -75,6 +80,30 @@ function renderAccountModal(overrides?: {
);
}
function buildSession(
overrides: Partial<AuthSessionSummary> = {},
): AuthSessionSummary {
return {
sessionId: 'usess_1',
sessionIds: ['usess_1'],
sessionCount: 1,
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',
...overrides,
};
}
test('settings header uses a generic title instead of the phone number', () => {
renderAccountModal();
@@ -238,8 +267,10 @@ test('account panel includes merged security devices and audit sections', async
},
],
sessions: [
{
buildSession({
sessionId: 'session-1',
sessionIds: ['session-1'],
sessionCount: 1,
clientType: 'mobile',
clientRuntime: 'ios',
clientPlatform: 'wechat',
@@ -253,7 +284,7 @@ test('account panel includes merged security devices and audit sections', async
lastSeenAt: '2026-04-20T09:00:00.000Z',
expiresAt: '2026-04-27T09:00:00.000Z',
ipMasked: '10.0.*.*',
},
}),
],
auditLogs: [
{
@@ -294,3 +325,77 @@ test('legacy nested section requests now open the merged account panel', () => {
expect(within(accountDialog).getByText('登录设备')).toBeTruthy();
expect(within(accountDialog).getByText('操作记录')).toBeTruthy();
});
test('current merged session group hides kick action and shows count', async () => {
const user = userEvent.setup();
renderAccountModal({
sessions: [
buildSession({
sessionId: 'usess_current',
sessionIds: ['usess_current', 'usess_rotated'],
sessionCount: 2,
isCurrent: true,
}),
],
});
await user.click(screen.getByRole('button', { name: /账号信息/ }));
const accountDialog = screen.getByRole('dialog', { name: '账号信息' });
expect(within(accountDialog).getByText('2 个会话')).toBeTruthy();
expect(
within(accountDialog).queryByRole('button', { name: '踢下线' }),
).toBeNull();
});
test('remote merged session group can be revoked with loading state', async () => {
const user = userEvent.setup();
const onRevokeSession = vi.fn().mockResolvedValue(undefined);
const remoteSession = buildSession({
sessionId: 'usess_remote',
sessionIds: ['usess_remote', 'usess_remote_rotated'],
sessionCount: 2,
});
renderAccountModal({
sessions: [remoteSession],
onRevokeSession,
revokingSessionIds: ['usess_remote'],
});
await user.click(screen.getByRole('button', { name: /账号信息/ }));
const accountDialog = screen.getByRole('dialog', { name: '账号信息' });
const revokeButton = within(accountDialog).getByRole('button', {
name: '处理中...',
}) as HTMLButtonElement;
expect(revokeButton.disabled).toBe(true);
expect(within(accountDialog).getByText('2 个会话')).toBeTruthy();
expect(onRevokeSession).not.toHaveBeenCalled();
});
test('remote session revoke passes the grouped session payload', async () => {
const user = userEvent.setup();
const onRevokeSession = vi.fn().mockResolvedValue(undefined);
const remoteSession = buildSession({
sessionId: 'usess_remote',
sessionIds: ['usess_remote', 'usess_remote_rotated'],
sessionCount: 2,
});
renderAccountModal({
sessions: [remoteSession],
onRevokeSession,
});
await user.click(screen.getByRole('button', { name: /账号信息/ }));
await user.click(
within(screen.getByRole('dialog', { name: '账号信息' })).getByRole(
'button',
{ name: '踢下线' },
),
);
expect(onRevokeSession).toHaveBeenCalledWith(remoteSession);
});