fix(web): 完善统一认证恢复与重配交互
为处理中、失败和清理中的配对展示明确状态与恢复动作,支持放弃并清理、作用域内退役冲突连接,并在 Active 状态引导管理员先安全禁用再使用新接入码。同步修复运行时切换后的浏览器会话残留与确认弹窗焦点约束。 验证:pnpm test;pnpm lint;pnpm build。
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import {
|
||||
cancelIdentityPairing,
|
||||
connectSecurityEventTransmitter,
|
||||
deleteOIDCBrowserSession,
|
||||
GatewayApiError,
|
||||
@@ -7,6 +8,7 @@ import {
|
||||
getCurrentUser,
|
||||
OIDC_BROWSER_SESSION_CREDENTIAL,
|
||||
startIdentityPairing,
|
||||
retireIdentityPairingSecurityEventConflict,
|
||||
validateIdentityRevision,
|
||||
} from './api';
|
||||
|
||||
@@ -22,6 +24,10 @@ describe('Gateway provisioning errors', () => {
|
||||
['OIDC_SESSION_REFRESH_UNAVAILABLE', '认证中心暂时不可用,请稍后重试'],
|
||||
['OIDC_SESSION_STORE_UNAVAILABLE', '登录会话存储暂时不可用,请稍后重试'],
|
||||
['OIDC_SESSION_CSRF_REJECTED', '登录会话来源校验失败,请刷新后重试'],
|
||||
['IDENTITY_PAIRING_IN_PROGRESS', '请先完成或放弃当前统一认证配对'],
|
||||
['IDENTITY_PAIRING_NOT_CANCELLABLE', '当前统一认证配置不能放弃'],
|
||||
['IDENTITY_PAIRING_CONFLICT_NOT_RESOLVABLE', '当前配对状态已变化,请刷新后重试'],
|
||||
['IDENTITY_VERSION_CONFLICT', '统一认证状态已变化,请刷新后重试'],
|
||||
] as const;
|
||||
|
||||
for (const [code, expected] of cases) {
|
||||
@@ -80,6 +86,46 @@ describe('identity configuration transport', () => {
|
||||
expect(new Headers(init.headers).get('If-Match')).toBe('W/"7"');
|
||||
expect(init.credentials).toBe('include');
|
||||
});
|
||||
|
||||
it('cancels a pairing with its own ETag and an idempotency key', async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue(new Response(JSON.stringify({
|
||||
id: 'pairing', status: 'cancelled', cleanupStatus: 'pending', version: 8,
|
||||
}), {
|
||||
status: 202,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}));
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
vi.stubGlobal('crypto', { randomUUID: () => '00000000-0000-4000-8000-000000000000' });
|
||||
|
||||
await cancelIdentityPairing('manager-token', 'pairing', 7);
|
||||
|
||||
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
|
||||
expect(url).toContain('/api/admin/system/identity/pairings/pairing/cancel');
|
||||
expect(init.method).toBe('POST');
|
||||
expect(init.body).toBeUndefined();
|
||||
expect(new Headers(init.headers).get('If-Match')).toBe('W/"7"');
|
||||
expect(new Headers(init.headers).get('Idempotency-Key')).toContain('identity-cancel-');
|
||||
expect(init.credentials).toBe('include');
|
||||
});
|
||||
|
||||
it('retires an SSF conflict through the pairing-scoped recovery endpoint', async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue(new Response(JSON.stringify({
|
||||
id: 'pairing', status: 'credentials_saved', version: 7,
|
||||
}), {
|
||||
status: 202,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}));
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
vi.stubGlobal('crypto', { randomUUID: () => '00000000-0000-4000-8000-000000000000' });
|
||||
|
||||
await retireIdentityPairingSecurityEventConflict('manager-token', 'pairing', 7);
|
||||
|
||||
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
|
||||
expect(url).toContain('/api/admin/system/identity/pairings/pairing/retire-conflicting-security-event');
|
||||
expect(init.method).toBe('POST');
|
||||
expect(new Headers(init.headers).get('If-Match')).toBe('W/"7"');
|
||||
expect(new Headers(init.headers).get('Idempotency-Key')).toContain('identity-retire-ssf-conflict-');
|
||||
});
|
||||
});
|
||||
|
||||
describe('security event connection transport', () => {
|
||||
|
||||
Reference in New Issue
Block a user