mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-13 07:40:59 +00:00
b53130ca0d
A sign-in with another subject of the same provider is refused, so an account whose provider re-issued the subject (a migrated or rebuilt IdP, a changed sub format) was locked out: editing the email, as the wiki advised, never touched the stored link. The edit dialog now offers "Remove SSO link" for other accounts while SSO is set up (DELETE /api/admin/users/:id/sso, audit admin.user_sso_unlink). The next SSO sign-in links again under the rules of a first one, so it still needs a verified, not self-set email. The log line names the remedy, and the wiki tells the two refusals apart. Fixes #3065
133 lines
5.1 KiB
TypeScript
133 lines
5.1 KiB
TypeScript
import { act, renderHook } from '@testing-library/react';
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import { useAdminUserActions } from './useAdminUserActions';
|
|
|
|
// FE-HOOK-ADMUSERACT-001 to FE-HOOK-ADMUSERACT-009
|
|
|
|
const api = vi.hoisted(() => ({
|
|
rotateJwtSecret: vi.fn(),
|
|
resetUserPasskeys: vi.fn(),
|
|
resetUserMfa: vi.fn(),
|
|
unlinkUserSso: vi.fn(),
|
|
}));
|
|
vi.mock('../../api/client', () => ({ adminApi: api }));
|
|
|
|
const t = (key: string, params?: Record<string, unknown>) => (params ? `${key} ${JSON.stringify(params)}` : key);
|
|
|
|
function host() {
|
|
return {
|
|
toast: { success: vi.fn(), error: vi.fn() },
|
|
setRotatingJwt: vi.fn(),
|
|
setShowRotateJwtModal: vi.fn(),
|
|
logout: vi.fn(),
|
|
navigate: vi.fn(),
|
|
};
|
|
}
|
|
|
|
function mount(admin = host()) {
|
|
return { admin, actions: renderHook(() => useAdminUserActions(admin, t)).result.current };
|
|
}
|
|
|
|
beforeEach(() => {
|
|
api.rotateJwtSecret.mockReset().mockResolvedValue({});
|
|
api.resetUserPasskeys.mockReset().mockResolvedValue({ deleted: 2 });
|
|
api.resetUserMfa.mockReset().mockResolvedValue({ success: true });
|
|
api.unlinkUserSso.mockReset().mockResolvedValue({ success: true, unlinked: true });
|
|
});
|
|
|
|
describe('useAdminUserActions', () => {
|
|
it('FE-HOOK-ADMUSERACT-001: rotating the secret closes the dialog, logs out and goes to the login page', async () => {
|
|
const { admin, actions } = mount();
|
|
await act(() => actions.rotateJwt());
|
|
expect(admin.setRotatingJwt).toHaveBeenCalledWith(true);
|
|
expect(admin.setShowRotateJwtModal).toHaveBeenCalledWith(false);
|
|
expect(admin.logout).toHaveBeenCalledTimes(1);
|
|
expect(admin.navigate).toHaveBeenCalledWith('/login', { state: { noRedirect: true } });
|
|
expect(admin.logout.mock.invocationCallOrder[0]).toBeLessThan(admin.navigate.mock.invocationCallOrder[0]);
|
|
});
|
|
|
|
it('FE-HOOK-ADMUSERACT-002: a failed rotation toasts, stays signed in and clears the busy state', async () => {
|
|
api.rotateJwtSecret.mockRejectedValue(new Error('x'));
|
|
const { admin, actions } = mount();
|
|
await act(() => actions.rotateJwt());
|
|
expect(admin.toast.error).toHaveBeenCalledWith('common.error');
|
|
expect(admin.setRotatingJwt.mock.calls).toEqual([[true], [false]]);
|
|
expect(admin.logout).not.toHaveBeenCalled();
|
|
expect(admin.setShowRotateJwtModal).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('FE-HOOK-ADMUSERACT-003: a passkey reset toasts the count and reports success', async () => {
|
|
const { admin, actions } = mount();
|
|
let ok: boolean | undefined;
|
|
await act(async () => {
|
|
ok = await actions.resetPasskeys({ id: 7 });
|
|
});
|
|
expect(api.resetUserPasskeys).toHaveBeenCalledWith(7);
|
|
expect(admin.toast.success).toHaveBeenCalledWith('admin.passkey.resetDone {"count":2}');
|
|
expect(ok).toBe(true);
|
|
});
|
|
|
|
it('FE-HOOK-ADMUSERACT-004: an answer without a count reads as zero', async () => {
|
|
api.resetUserPasskeys.mockResolvedValue({});
|
|
const { admin, actions } = mount();
|
|
await act(() => actions.resetPasskeys({ id: 7 }));
|
|
expect(admin.toast.success).toHaveBeenCalledWith('admin.passkey.resetDone {"count":0}');
|
|
});
|
|
|
|
it('FE-HOOK-ADMUSERACT-005: a failed passkey reset toasts and reports failure', async () => {
|
|
api.resetUserPasskeys.mockRejectedValue(new Error('x'));
|
|
const { admin, actions } = mount();
|
|
let ok: boolean | undefined;
|
|
await act(async () => {
|
|
ok = await actions.resetPasskeys({ id: 7 });
|
|
});
|
|
expect(admin.toast.error).toHaveBeenCalledWith('common.error');
|
|
expect(ok).toBe(false);
|
|
});
|
|
|
|
it('FE-HOOK-ADMUSERACT-006: a 2FA reset calls the API, toasts and reports success', async () => {
|
|
const { admin, actions } = mount();
|
|
let ok: boolean | undefined;
|
|
await act(async () => {
|
|
ok = await actions.resetMfa({ id: 9 });
|
|
});
|
|
expect(api.resetUserMfa).toHaveBeenCalledWith(9);
|
|
expect(admin.toast.success).toHaveBeenCalledWith('admin.mfa.resetDone');
|
|
expect(ok).toBe(true);
|
|
});
|
|
|
|
it('FE-HOOK-ADMUSERACT-007: a failed 2FA reset toasts and reports failure', async () => {
|
|
api.resetUserMfa.mockRejectedValue(new Error('x'));
|
|
const { admin, actions } = mount();
|
|
let ok: boolean | undefined;
|
|
await act(async () => {
|
|
ok = await actions.resetMfa({ id: 9 });
|
|
});
|
|
expect(admin.toast.error).toHaveBeenCalledWith('common.error');
|
|
expect(ok).toBe(false);
|
|
});
|
|
|
|
it('FE-HOOK-ADMUSERACT-008: removing the SSO link calls the API, toasts and reports success', async () => {
|
|
const { admin, actions } = mount();
|
|
let ok: boolean | undefined;
|
|
await act(async () => {
|
|
ok = await actions.unlinkSso({ id: 11 });
|
|
});
|
|
expect(api.unlinkUserSso).toHaveBeenCalledWith(11);
|
|
expect(admin.toast.success).toHaveBeenCalledWith('admin.sso.unlinkDone');
|
|
expect(ok).toBe(true);
|
|
});
|
|
|
|
it('FE-HOOK-ADMUSERACT-009: a failed SSO unlink toasts and reports failure', async () => {
|
|
api.unlinkUserSso.mockRejectedValue(new Error('x'));
|
|
const { admin, actions } = mount();
|
|
let ok: boolean | undefined;
|
|
await act(async () => {
|
|
ok = await actions.unlinkSso({ id: 11 });
|
|
});
|
|
expect(admin.toast.error).toHaveBeenCalledWith('common.error');
|
|
expect(ok).toBe(false);
|
|
});
|
|
});
|