252 lines
8.0 KiB
TypeScript
252 lines
8.0 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import { renderHook, waitFor } from '@testing-library/react';
|
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
import { ReactNode } from 'react';
|
|
import {
|
|
useSecurityNotificationSettings,
|
|
useUpdateSecurityNotificationSettings,
|
|
} from '../useNotifications';
|
|
import * as notificationsApi from '../../api/notifications';
|
|
|
|
// Mock the API
|
|
vi.mock('../../api/notifications', async () => {
|
|
const actual = await vi.importActual('../../api/notifications');
|
|
return {
|
|
...actual,
|
|
getSecurityNotificationSettings: vi.fn(),
|
|
updateSecurityNotificationSettings: vi.fn(),
|
|
};
|
|
});
|
|
|
|
// Mock toast
|
|
vi.mock('../../utils/toast', () => ({
|
|
toast: {
|
|
success: vi.fn(),
|
|
error: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
describe('useNotifications hooks', () => {
|
|
let queryClient: QueryClient;
|
|
|
|
const createWrapper = () => {
|
|
return ({ children }: { children: ReactNode }) => (
|
|
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
|
);
|
|
};
|
|
|
|
beforeEach(() => {
|
|
queryClient = new QueryClient({
|
|
defaultOptions: {
|
|
queries: { retry: false },
|
|
mutations: { retry: false },
|
|
},
|
|
});
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
describe('useSecurityNotificationSettings', () => {
|
|
it('fetches security notification settings', async () => {
|
|
const mockSettings: notificationsApi.SecurityNotificationSettings = {
|
|
enabled: true,
|
|
min_log_level: 'warn',
|
|
notify_waf_blocks: true,
|
|
notify_acl_denials: true,
|
|
notify_rate_limit_hits: false,
|
|
webhook_url: 'https://example.com/webhook',
|
|
email_recipients: 'admin@example.com',
|
|
};
|
|
|
|
vi.mocked(notificationsApi.getSecurityNotificationSettings).mockResolvedValue(mockSettings);
|
|
|
|
const { result } = renderHook(() => useSecurityNotificationSettings(), {
|
|
wrapper: createWrapper(),
|
|
});
|
|
|
|
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
|
|
|
expect(result.current.data).toEqual(mockSettings);
|
|
expect(notificationsApi.getSecurityNotificationSettings).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('handles fetch errors', async () => {
|
|
vi.mocked(notificationsApi.getSecurityNotificationSettings).mockRejectedValue(
|
|
new Error('Network error')
|
|
);
|
|
|
|
const { result } = renderHook(() => useSecurityNotificationSettings(), {
|
|
wrapper: createWrapper(),
|
|
});
|
|
|
|
await waitFor(() => expect(result.current.isError).toBe(true));
|
|
|
|
expect(result.current.error).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
describe('useUpdateSecurityNotificationSettings', () => {
|
|
const mockSettings: notificationsApi.SecurityNotificationSettings = {
|
|
enabled: true,
|
|
min_log_level: 'warn',
|
|
notify_waf_blocks: true,
|
|
notify_acl_denials: true,
|
|
notify_rate_limit_hits: false,
|
|
};
|
|
|
|
beforeEach(() => {
|
|
vi.mocked(notificationsApi.getSecurityNotificationSettings).mockResolvedValue(mockSettings);
|
|
});
|
|
|
|
it('updates security notification settings', async () => {
|
|
const updatedSettings = { ...mockSettings, min_log_level: 'error' };
|
|
vi.mocked(notificationsApi.updateSecurityNotificationSettings).mockResolvedValue(
|
|
updatedSettings
|
|
);
|
|
|
|
const { result } = renderHook(() => useUpdateSecurityNotificationSettings(), {
|
|
wrapper: createWrapper(),
|
|
});
|
|
|
|
result.current.mutate({ min_log_level: 'error' });
|
|
|
|
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
|
|
|
expect(notificationsApi.updateSecurityNotificationSettings).toHaveBeenCalledWith({
|
|
min_log_level: 'error',
|
|
});
|
|
});
|
|
|
|
it('performs optimistic update', async () => {
|
|
const updatedSettings = { ...mockSettings, enabled: false };
|
|
vi.mocked(notificationsApi.updateSecurityNotificationSettings).mockResolvedValue(
|
|
updatedSettings
|
|
);
|
|
|
|
// Pre-populate cache
|
|
queryClient.setQueryData(['security-notification-settings'], mockSettings);
|
|
|
|
const { result } = renderHook(() => useUpdateSecurityNotificationSettings(), {
|
|
wrapper: createWrapper(),
|
|
});
|
|
|
|
result.current.mutate({ enabled: false });
|
|
|
|
// Wait a bit for the optimistic update to take effect
|
|
await waitFor(() => {
|
|
const cachedData = queryClient.getQueryData(['security-notification-settings']);
|
|
expect(cachedData).toMatchObject({ enabled: false });
|
|
});
|
|
|
|
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
|
});
|
|
|
|
it('rolls back on error', async () => {
|
|
vi.mocked(notificationsApi.updateSecurityNotificationSettings).mockRejectedValue(
|
|
new Error('Update failed')
|
|
);
|
|
|
|
// Pre-populate cache
|
|
queryClient.setQueryData(['security-notification-settings'], mockSettings);
|
|
|
|
const { result } = renderHook(() => useUpdateSecurityNotificationSettings(), {
|
|
wrapper: createWrapper(),
|
|
});
|
|
|
|
result.current.mutate({ enabled: false });
|
|
|
|
await waitFor(() => expect(result.current.isError).toBe(true));
|
|
|
|
// Check that original data is restored
|
|
const cachedData = queryClient.getQueryData(['security-notification-settings']);
|
|
expect(cachedData).toEqual(mockSettings);
|
|
});
|
|
|
|
it('shows success toast on successful update', async () => {
|
|
const toast = await import('../../utils/toast');
|
|
const updatedSettings = { ...mockSettings, min_log_level: 'error' };
|
|
vi.mocked(notificationsApi.updateSecurityNotificationSettings).mockResolvedValue(
|
|
updatedSettings
|
|
);
|
|
|
|
const { result } = renderHook(() => useUpdateSecurityNotificationSettings(), {
|
|
wrapper: createWrapper(),
|
|
});
|
|
|
|
result.current.mutate({ min_log_level: 'error' });
|
|
|
|
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
|
|
|
expect(toast.toast.success).toHaveBeenCalledWith('Notification settings updated');
|
|
});
|
|
|
|
it('shows error toast on failed update', async () => {
|
|
const toast = await import('../../utils/toast');
|
|
vi.mocked(notificationsApi.updateSecurityNotificationSettings).mockRejectedValue(
|
|
new Error('Update failed')
|
|
);
|
|
|
|
const { result } = renderHook(() => useUpdateSecurityNotificationSettings(), {
|
|
wrapper: createWrapper(),
|
|
});
|
|
|
|
result.current.mutate({ enabled: false });
|
|
|
|
await waitFor(() => expect(result.current.isError).toBe(true));
|
|
|
|
expect(toast.toast.error).toHaveBeenCalledWith('Update failed');
|
|
});
|
|
|
|
it('invalidates queries on success', async () => {
|
|
const updatedSettings = { ...mockSettings, min_log_level: 'error' };
|
|
vi.mocked(notificationsApi.updateSecurityNotificationSettings).mockResolvedValue(
|
|
updatedSettings
|
|
);
|
|
|
|
const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries');
|
|
|
|
const { result } = renderHook(() => useUpdateSecurityNotificationSettings(), {
|
|
wrapper: createWrapper(),
|
|
});
|
|
|
|
result.current.mutate({ min_log_level: 'error' });
|
|
|
|
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
|
|
|
expect(invalidateSpy).toHaveBeenCalledWith({
|
|
queryKey: ['security-notification-settings'],
|
|
});
|
|
});
|
|
|
|
it('handles updates with multiple fields', async () => {
|
|
const updatedSettings = {
|
|
...mockSettings,
|
|
enabled: false,
|
|
min_log_level: 'error',
|
|
webhook_url: 'https://new-webhook.com',
|
|
};
|
|
|
|
vi.mocked(notificationsApi.updateSecurityNotificationSettings).mockResolvedValue(
|
|
updatedSettings
|
|
);
|
|
|
|
const { result } = renderHook(() => useUpdateSecurityNotificationSettings(), {
|
|
wrapper: createWrapper(),
|
|
});
|
|
|
|
result.current.mutate({
|
|
enabled: false,
|
|
min_log_level: 'error',
|
|
webhook_url: 'https://new-webhook.com',
|
|
});
|
|
|
|
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
|
|
|
expect(notificationsApi.updateSecurityNotificationSettings).toHaveBeenCalledWith({
|
|
enabled: false,
|
|
min_log_level: 'error',
|
|
webhook_url: 'https://new-webhook.com',
|
|
});
|
|
});
|
|
});
|
|
});
|