feat: Add comprehensive test suite and misc improvements

- Add frontend unit tests with Vitest for components, hooks, pages, and utilities
- Add backend tests for webhooks, notifications, middleware, and edge cases
- Add ForgotPassword, NotFound, and ResetPassword pages
- Add migration for orphaned staff resources conversion
- Add coverage directory to gitignore (generated reports)
- Various bug fixes and improvements from previous work

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
poduck
2025-12-08 02:36:46 -05:00
parent c220612214
commit 8dc2248f1f
145 changed files with 77947 additions and 1048 deletions

View File

@@ -0,0 +1,142 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { renderHook, waitFor, act } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import React from 'react';
// Mock the notifications API
vi.mock('../../api/notifications', () => ({
getNotifications: vi.fn(),
getUnreadCount: vi.fn(),
markNotificationRead: vi.fn(),
markAllNotificationsRead: vi.fn(),
clearAllNotifications: vi.fn(),
}));
import {
useNotifications,
useUnreadNotificationCount,
useMarkNotificationRead,
useMarkAllNotificationsRead,
useClearAllNotifications,
} from '../useNotifications';
import * as notificationsApi from '../../api/notifications';
// Create wrapper
const createWrapper = () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});
return function Wrapper({ children }: { children: React.ReactNode }) {
return React.createElement(QueryClientProvider, { client: queryClient }, children);
};
};
describe('useNotifications hooks', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('useNotifications', () => {
it('fetches notifications', async () => {
const mockNotifications = [
{ id: 1, verb: 'created', read: false, timestamp: '2024-01-01T00:00:00Z' },
];
vi.mocked(notificationsApi.getNotifications).mockResolvedValue(mockNotifications);
const { result } = renderHook(() => useNotifications(), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(notificationsApi.getNotifications).toHaveBeenCalledWith(undefined);
expect(result.current.data).toEqual(mockNotifications);
});
it('passes options to API', async () => {
vi.mocked(notificationsApi.getNotifications).mockResolvedValue([]);
renderHook(() => useNotifications({ read: false, limit: 10 }), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(notificationsApi.getNotifications).toHaveBeenCalledWith({
read: false,
limit: 10,
});
});
});
});
describe('useUnreadNotificationCount', () => {
it('fetches unread count', async () => {
vi.mocked(notificationsApi.getUnreadCount).mockResolvedValue(5);
const { result } = renderHook(() => useUnreadNotificationCount(), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(result.current.data).toBe(5);
});
});
describe('useMarkNotificationRead', () => {
it('marks notification as read', async () => {
vi.mocked(notificationsApi.markNotificationRead).mockResolvedValue(undefined);
const { result } = renderHook(() => useMarkNotificationRead(), {
wrapper: createWrapper(),
});
await act(async () => {
await result.current.mutateAsync(42);
});
expect(notificationsApi.markNotificationRead).toHaveBeenCalled();
expect(vi.mocked(notificationsApi.markNotificationRead).mock.calls[0][0]).toBe(42);
});
});
describe('useMarkAllNotificationsRead', () => {
it('marks all notifications as read', async () => {
vi.mocked(notificationsApi.markAllNotificationsRead).mockResolvedValue(undefined);
const { result } = renderHook(() => useMarkAllNotificationsRead(), {
wrapper: createWrapper(),
});
await act(async () => {
await result.current.mutateAsync();
});
expect(notificationsApi.markAllNotificationsRead).toHaveBeenCalled();
});
});
describe('useClearAllNotifications', () => {
it('clears all notifications', async () => {
vi.mocked(notificationsApi.clearAllNotifications).mockResolvedValue(undefined);
const { result } = renderHook(() => useClearAllNotifications(), {
wrapper: createWrapper(),
});
await act(async () => {
await result.current.mutateAsync();
});
expect(notificationsApi.clearAllNotifications).toHaveBeenCalled();
});
});
});