mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-12 23:31:02 +00:00
7da3a1724c
useMcpTokensAdmin now loads both lists, revokes sessions, deletes tokens and keeps the expanded scope chips. Both panels keep their own cards and confirm dialogs.
141 lines
5.7 KiB
TypeScript
141 lines
5.7 KiB
TypeScript
import { act, renderHook, waitFor } from '@testing-library/react';
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import { adminApi } from '../../api/client';
|
|
import { useMcpTokensAdmin } from './useMcpTokensAdmin';
|
|
|
|
// FE-HOOK-MCPADMIN-001 to FE-HOOK-MCPADMIN-008
|
|
|
|
const toast = vi.hoisted(() => ({ success: vi.fn(), error: vi.fn() }));
|
|
const i18n = vi.hoisted(() => ({ prefix: '' }));
|
|
vi.mock('../shared/Toast', () => ({ useToast: () => toast }));
|
|
// Each render's t keeps the prefix it was made with, like a real locale switch.
|
|
vi.mock('../../i18n', () => ({
|
|
useTranslation: () => {
|
|
const prefix = i18n.prefix;
|
|
return { t: (k: string) => `${prefix}${k}` };
|
|
},
|
|
}));
|
|
|
|
const SESSION = {
|
|
id: 11,
|
|
client_id: 'c',
|
|
client_name: 'Claude',
|
|
user_id: 1,
|
|
username: 'alice',
|
|
scopes: ['trips:read'],
|
|
access_token_expires_at: '2026-01-01',
|
|
refresh_token_expires_at: '2026-02-01',
|
|
created_at: '2025-01-01',
|
|
};
|
|
const TOKEN = {
|
|
id: 21,
|
|
name: 'cli',
|
|
token_prefix: 'trek_ab',
|
|
created_at: '2025-01-01',
|
|
last_used_at: null,
|
|
user_id: 1,
|
|
username: 'alice',
|
|
};
|
|
|
|
beforeEach(() => {
|
|
i18n.prefix = '';
|
|
toast.success.mockReset();
|
|
toast.error.mockReset();
|
|
vi.spyOn(adminApi, 'oauthSessions').mockResolvedValue({ sessions: [SESSION] });
|
|
vi.spyOn(adminApi, 'mcpTokens').mockResolvedValue({ tokens: [TOKEN] });
|
|
vi.spyOn(adminApi, 'revokeOAuthSession').mockResolvedValue({ success: true });
|
|
vi.spyOn(adminApi, 'deleteMcpToken').mockResolvedValue({ success: true });
|
|
});
|
|
afterEach(() => vi.restoreAllMocks());
|
|
|
|
async function loaded() {
|
|
const hook = renderHook(() => useMcpTokensAdmin());
|
|
await waitFor(() => {
|
|
expect(hook.result.current.sessionsLoading).toBe(false);
|
|
expect(hook.result.current.tokensLoading).toBe(false);
|
|
});
|
|
return hook;
|
|
}
|
|
|
|
describe('useMcpTokensAdmin', () => {
|
|
it('FE-HOOK-MCPADMIN-001: loads sessions and tokens once on mount', async () => {
|
|
const { result, rerender } = await loaded();
|
|
rerender();
|
|
expect(adminApi.oauthSessions).toHaveBeenCalledTimes(1);
|
|
expect(adminApi.mcpTokens).toHaveBeenCalledTimes(1);
|
|
expect(result.current.sessions).toEqual([SESSION]);
|
|
expect(result.current.tokens).toEqual([TOKEN]);
|
|
});
|
|
|
|
it('FE-HOOK-MCPADMIN-002: missing lists read as empty', async () => {
|
|
vi.mocked(adminApi.oauthSessions).mockResolvedValue({});
|
|
vi.mocked(adminApi.mcpTokens).mockResolvedValue({});
|
|
const { result } = await loaded();
|
|
expect(result.current.sessions).toEqual([]);
|
|
expect(result.current.tokens).toEqual([]);
|
|
});
|
|
|
|
it('FE-HOOK-MCPADMIN-003: each failed list toasts its own error, in the locale current when it fails', async () => {
|
|
let failSessions!: (e: unknown) => void;
|
|
vi.mocked(adminApi.oauthSessions).mockReturnValue(new Promise((_, reject) => (failSessions = reject)));
|
|
vi.mocked(adminApi.mcpTokens).mockRejectedValue(new Error('down'));
|
|
const { result, rerender } = renderHook(() => useMcpTokensAdmin());
|
|
await waitFor(() => expect(result.current.tokensLoading).toBe(false));
|
|
expect(toast.error).toHaveBeenCalledWith('admin.mcpTokens.loadError');
|
|
i18n.prefix = 'de:';
|
|
rerender();
|
|
await act(async () => failSessions(new Error('down')));
|
|
await waitFor(() => expect(result.current.sessionsLoading).toBe(false));
|
|
expect(toast.error).toHaveBeenCalledWith('de:admin.oauthSessions.loadError');
|
|
});
|
|
|
|
it('FE-HOOK-MCPADMIN-004: toggling scopes expands and collapses one session', async () => {
|
|
const { result } = await loaded();
|
|
act(() => result.current.toggleScopes(11));
|
|
expect(result.current.expandedScopes.has(11)).toBe(true);
|
|
act(() => result.current.toggleScopes(11));
|
|
expect(result.current.expandedScopes.has(11)).toBe(false);
|
|
});
|
|
|
|
it('FE-HOOK-MCPADMIN-005: revoke drops the session, closes the confirm and toasts', async () => {
|
|
const { result } = await loaded();
|
|
act(() => result.current.setRevokeConfirmId(11));
|
|
await act(() => result.current.handleRevoke(11));
|
|
expect(adminApi.revokeOAuthSession).toHaveBeenCalledWith(11);
|
|
expect(result.current.sessions).toEqual([]);
|
|
expect(result.current.revokeConfirmId).toBeNull();
|
|
expect(toast.success).toHaveBeenCalledWith('admin.oauthSessions.revokeSuccess');
|
|
});
|
|
|
|
it('FE-HOOK-MCPADMIN-006: a failed revoke keeps the session and the confirm open', async () => {
|
|
vi.mocked(adminApi.revokeOAuthSession).mockRejectedValue(new Error('nope'));
|
|
const { result } = await loaded();
|
|
act(() => result.current.setRevokeConfirmId(11));
|
|
await act(() => result.current.handleRevoke(11));
|
|
expect(result.current.sessions).toEqual([SESSION]);
|
|
expect(result.current.revokeConfirmId).toBe(11);
|
|
expect(toast.error).toHaveBeenCalledWith('admin.oauthSessions.revokeError');
|
|
});
|
|
|
|
it('FE-HOOK-MCPADMIN-007: delete drops the token, closes the confirm and toasts', async () => {
|
|
const { result } = await loaded();
|
|
act(() => result.current.setDeleteConfirmId(21));
|
|
await act(() => result.current.handleDelete(21));
|
|
expect(adminApi.deleteMcpToken).toHaveBeenCalledWith(21);
|
|
expect(result.current.tokens).toEqual([]);
|
|
expect(result.current.deleteConfirmId).toBeNull();
|
|
expect(toast.success).toHaveBeenCalledWith('admin.mcpTokens.deleteSuccess');
|
|
});
|
|
|
|
it('FE-HOOK-MCPADMIN-008: a failed delete keeps the token and the confirm open', async () => {
|
|
vi.mocked(adminApi.deleteMcpToken).mockRejectedValue(new Error('nope'));
|
|
const { result } = await loaded();
|
|
act(() => result.current.setDeleteConfirmId(21));
|
|
await act(() => result.current.handleDelete(21));
|
|
expect(result.current.tokens).toEqual([TOKEN]);
|
|
expect(result.current.deleteConfirmId).toBe(21);
|
|
expect(toast.error).toHaveBeenCalledWith('admin.mcpTokens.deleteError');
|
|
});
|
|
});
|