- Introduced unit tests for individual and bulk session revocation in `SessionsManager` with success callback assertions. - Added `/* istanbul ignore */` annotations to metadata and design system pages covered by e2e tests.
312 lines
8.8 KiB
TypeScript
312 lines
8.8 KiB
TypeScript
/**
|
|
* Tests for SessionsManager Component
|
|
*/
|
|
|
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
import { SessionsManager } from '@/components/settings/SessionsManager';
|
|
import * as useSessionModule from '@/lib/api/hooks/useSession';
|
|
|
|
jest.mock('@/lib/api/hooks/useSession');
|
|
jest.mock('sonner', () => ({ toast: { success: jest.fn(), error: jest.fn() } }));
|
|
|
|
const mockUseListSessions = useSessionModule.useListSessions as jest.Mock;
|
|
const mockUseRevokeSession = useSessionModule.useRevokeSession as jest.Mock;
|
|
const mockUseRevokeAllOtherSessions = useSessionModule.useRevokeAllOtherSessions as jest.Mock;
|
|
|
|
describe('SessionsManager', () => {
|
|
let queryClient: QueryClient;
|
|
const mockRevokeMutate = jest.fn();
|
|
const mockRevokeAllMutate = jest.fn();
|
|
|
|
const mockSessions = [
|
|
{
|
|
id: '1',
|
|
device_name: 'Chrome on Mac',
|
|
ip_address: '192.168.1.1',
|
|
location_city: 'San Francisco',
|
|
location_country: 'USA',
|
|
last_used_at: '2024-01-01T12:00:00Z',
|
|
created_at: '2024-01-01T00:00:00Z',
|
|
expires_at: '2024-01-08T00:00:00Z',
|
|
is_current: true,
|
|
},
|
|
{
|
|
id: '2',
|
|
device_name: 'Firefox on Windows',
|
|
ip_address: '192.168.1.2',
|
|
location_city: 'New York',
|
|
location_country: 'USA',
|
|
last_used_at: '2024-01-01T11:00:00Z',
|
|
created_at: '2023-12-31T00:00:00Z',
|
|
expires_at: '2024-01-07T00:00:00Z',
|
|
is_current: false,
|
|
},
|
|
];
|
|
|
|
beforeEach(() => {
|
|
queryClient = new QueryClient({
|
|
defaultOptions: { queries: { retry: false }, mutations: { retry: false } },
|
|
});
|
|
|
|
jest.clearAllMocks();
|
|
|
|
mockUseRevokeSession.mockReturnValue({
|
|
mutate: mockRevokeMutate,
|
|
isPending: false,
|
|
});
|
|
|
|
mockUseRevokeAllOtherSessions.mockReturnValue({
|
|
mutate: mockRevokeAllMutate,
|
|
isPending: false,
|
|
});
|
|
});
|
|
|
|
const renderWithProvider = (component: React.ReactElement) =>
|
|
render(<QueryClientProvider client={queryClient}>{component}</QueryClientProvider>);
|
|
|
|
it('shows loading state', () => {
|
|
mockUseListSessions.mockReturnValue({
|
|
data: undefined,
|
|
isLoading: true,
|
|
error: null,
|
|
});
|
|
|
|
renderWithProvider(<SessionsManager />);
|
|
|
|
expect(screen.getByText(/loading your active sessions/i)).toBeInTheDocument();
|
|
});
|
|
|
|
it('shows error state', () => {
|
|
mockUseListSessions.mockReturnValue({
|
|
data: undefined,
|
|
isLoading: false,
|
|
error: new Error('Failed to load'),
|
|
});
|
|
|
|
renderWithProvider(<SessionsManager />);
|
|
|
|
expect(screen.getByText(/unable to load your sessions/i)).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders sessions list', () => {
|
|
mockUseListSessions.mockReturnValue({
|
|
data: mockSessions,
|
|
isLoading: false,
|
|
error: null,
|
|
});
|
|
|
|
renderWithProvider(<SessionsManager />);
|
|
|
|
expect(screen.getByText('Chrome on Mac')).toBeInTheDocument();
|
|
expect(screen.getByText('Firefox on Windows')).toBeInTheDocument();
|
|
});
|
|
|
|
it('shows "Revoke All Others" button when multiple sessions exist', () => {
|
|
mockUseListSessions.mockReturnValue({
|
|
data: mockSessions,
|
|
isLoading: false,
|
|
error: null,
|
|
});
|
|
|
|
renderWithProvider(<SessionsManager />);
|
|
|
|
expect(screen.getByRole('button', { name: /revoke all others/i })).toBeInTheDocument();
|
|
});
|
|
|
|
it('does not show "Revoke All Others" when only current session', () => {
|
|
mockUseListSessions.mockReturnValue({
|
|
data: [mockSessions[0]], // Only current session
|
|
isLoading: false,
|
|
error: null,
|
|
});
|
|
|
|
renderWithProvider(<SessionsManager />);
|
|
|
|
expect(screen.queryByRole('button', { name: /revoke all others/i })).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('opens bulk revoke dialog', async () => {
|
|
mockUseListSessions.mockReturnValue({
|
|
data: mockSessions,
|
|
isLoading: false,
|
|
error: null,
|
|
});
|
|
|
|
renderWithProvider(<SessionsManager />);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: /revoke all others/i }));
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Revoke All Other Sessions?')).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('calls bulk revoke when confirmed', async () => {
|
|
mockUseListSessions.mockReturnValue({
|
|
data: mockSessions,
|
|
isLoading: false,
|
|
error: null,
|
|
});
|
|
|
|
renderWithProvider(<SessionsManager />);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: /revoke all others/i }));
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Revoke All Other Sessions?')).toBeInTheDocument();
|
|
});
|
|
|
|
// Find the destructive button in the dialog (not the "Cancel" button)
|
|
const buttons = screen.getAllByRole('button');
|
|
const confirmButton = buttons.find((btn) => btn.textContent === 'Revoke All Others');
|
|
|
|
if (confirmButton) {
|
|
fireEvent.click(confirmButton);
|
|
}
|
|
|
|
expect(mockRevokeAllMutate).toHaveBeenCalled();
|
|
});
|
|
|
|
it('calls revoke when individual session revoke clicked', async () => {
|
|
mockUseListSessions.mockReturnValue({
|
|
data: mockSessions,
|
|
isLoading: false,
|
|
error: null,
|
|
});
|
|
|
|
renderWithProvider(<SessionsManager />);
|
|
|
|
// Click the individual "Revoke" button (not "Revoke All Others")
|
|
const revokeButtons = screen.getAllByRole('button', { name: /^revoke$/i });
|
|
fireEvent.click(revokeButtons[0]);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Revoke Session?')).toBeInTheDocument();
|
|
});
|
|
|
|
const confirmButton = screen.getByRole('button', { name: /revoke session/i });
|
|
fireEvent.click(confirmButton);
|
|
|
|
expect(mockRevokeMutate).toHaveBeenCalledWith('2');
|
|
});
|
|
|
|
it('shows empty state when no sessions', () => {
|
|
mockUseListSessions.mockReturnValue({
|
|
data: [],
|
|
isLoading: false,
|
|
error: null,
|
|
});
|
|
|
|
renderWithProvider(<SessionsManager />);
|
|
|
|
expect(screen.getByText('No active sessions to display')).toBeInTheDocument();
|
|
});
|
|
|
|
it('shows info message when only one session', () => {
|
|
mockUseListSessions.mockReturnValue({
|
|
data: [mockSessions[0]],
|
|
isLoading: false,
|
|
error: null,
|
|
});
|
|
|
|
renderWithProvider(<SessionsManager />);
|
|
|
|
expect(screen.getByText(/you're viewing your only active session/i)).toBeInTheDocument();
|
|
});
|
|
|
|
it('shows security tip', () => {
|
|
mockUseListSessions.mockReturnValue({
|
|
data: mockSessions,
|
|
isLoading: false,
|
|
error: null,
|
|
});
|
|
|
|
renderWithProvider(<SessionsManager />);
|
|
|
|
expect(screen.getByText(/security tip/i)).toBeInTheDocument();
|
|
expect(screen.getByText(/if you see a session you don't recognize/i)).toBeInTheDocument();
|
|
});
|
|
|
|
it('closes bulk revoke dialog on cancel', async () => {
|
|
mockUseListSessions.mockReturnValue({
|
|
data: mockSessions,
|
|
isLoading: false,
|
|
error: null,
|
|
});
|
|
|
|
renderWithProvider(<SessionsManager />);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: /revoke all others/i }));
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Revoke All Other Sessions?')).toBeInTheDocument();
|
|
});
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: /cancel/i }));
|
|
|
|
await waitFor(() => {
|
|
expect(screen.queryByText('Revoke All Other Sessions?')).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('calls toast.success on successful individual session revoke', () => {
|
|
const { toast } = require('sonner');
|
|
let successCallback: ((message: string) => void) | undefined;
|
|
|
|
// Mock useRevokeSession to capture the success callback
|
|
mockUseRevokeSession.mockImplementation((onSuccess) => {
|
|
successCallback = onSuccess;
|
|
return {
|
|
mutate: mockRevokeMutate,
|
|
isPending: false,
|
|
};
|
|
});
|
|
|
|
mockUseListSessions.mockReturnValue({
|
|
data: mockSessions,
|
|
isLoading: false,
|
|
error: null,
|
|
});
|
|
|
|
renderWithProvider(<SessionsManager />);
|
|
|
|
// Trigger the success callback
|
|
if (successCallback) {
|
|
successCallback('Session revoked successfully');
|
|
}
|
|
|
|
expect(toast.success).toHaveBeenCalledWith('Session revoked successfully');
|
|
});
|
|
|
|
it('calls toast.success and closes dialog on successful bulk revoke', () => {
|
|
const { toast } = require('sonner');
|
|
let successCallback: ((message: string) => void) | undefined;
|
|
|
|
// Mock useRevokeAllOtherSessions to capture the success callback
|
|
mockUseRevokeAllOtherSessions.mockImplementation((onSuccess) => {
|
|
successCallback = onSuccess;
|
|
return {
|
|
mutate: mockRevokeAllMutate,
|
|
isPending: false,
|
|
};
|
|
});
|
|
|
|
mockUseListSessions.mockReturnValue({
|
|
data: mockSessions,
|
|
isLoading: false,
|
|
error: null,
|
|
});
|
|
|
|
renderWithProvider(<SessionsManager />);
|
|
|
|
// Trigger the success callback
|
|
if (successCallback) {
|
|
successCallback('All other sessions revoked');
|
|
}
|
|
|
|
expect(toast.success).toHaveBeenCalledWith('All other sessions revoked');
|
|
// The callback also calls setShowBulkRevokeDialog(false)
|
|
});
|
|
});
|