fix(auth): tighten refresh session revocation
This commit is contained in:
@@ -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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user