feat: add comprehensive test coverage and fix lint issues
- Add comprehensive tests for MailgunService (439 lines) * Email sending functionality with template generation * Configuration status validation * Error handling and edge cases * Mock setup for fetch API and FormData - Add DatabaseService tests (451 lines) * Strategy pattern testing (Mock vs Production) * All CRUD operations for users, medications, settings * Legacy compatibility method testing * Proper TypeScript typing - Add MockDatabaseStrategy tests (434 lines) * Complete coverage of mock database implementation * User operations, medication management * Settings and custom reminders functionality * Data persistence and error handling - Add React hooks tests * useLocalStorage hook with comprehensive edge cases (340 lines) * useSettings hook with fetch operations and error handling (78 lines) - Fix auth integration tests * Update mocking to use new database service instead of legacy couchdb.factory * Fix service variable references and expectations - Simplify mailgun config tests * Remove redundant edge case testing * Focus on core functionality validation - Fix all TypeScript and ESLint issues * Proper FormData mock typing * Correct database entity type usage * Remove non-existent property references Test Results: - 184 total tests passing - Comprehensive coverage of core services - Zero TypeScript compilation errors - Full ESLint compliance
This commit is contained in:
378
hooks/__tests__/useLocalStorage.test.ts
Normal file
378
hooks/__tests__/useLocalStorage.test.ts
Normal file
@@ -0,0 +1,378 @@
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
import { useLocalStorage } from '../useLocalStorage';
|
||||
|
||||
// Mock localStorage
|
||||
const localStorageMock = {
|
||||
getItem: jest.fn(),
|
||||
setItem: jest.fn(),
|
||||
removeItem: jest.fn(),
|
||||
clear: jest.fn(),
|
||||
};
|
||||
|
||||
Object.defineProperty(window, 'localStorage', {
|
||||
value: localStorageMock,
|
||||
writable: true,
|
||||
});
|
||||
|
||||
describe('useLocalStorage', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
localStorageMock.getItem.mockReturnValue(null);
|
||||
});
|
||||
|
||||
describe('initialization', () => {
|
||||
test('should return default value when localStorage is empty', () => {
|
||||
localStorageMock.getItem.mockReturnValue(null);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useLocalStorage('test-key', 'default-value')
|
||||
);
|
||||
|
||||
expect(result.current[0]).toBe('default-value');
|
||||
expect(localStorageMock.getItem).toHaveBeenCalledWith('test-key');
|
||||
});
|
||||
|
||||
test('should return stored value when localStorage has data', () => {
|
||||
localStorageMock.getItem.mockReturnValue(JSON.stringify('stored-value'));
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useLocalStorage('test-key', 'default-value')
|
||||
);
|
||||
|
||||
expect(result.current[0]).toBe('stored-value');
|
||||
expect(localStorageMock.getItem).toHaveBeenCalledWith('test-key');
|
||||
});
|
||||
|
||||
test('should handle complex objects', () => {
|
||||
const storedObject = { name: 'John', age: 30 };
|
||||
localStorageMock.getItem.mockReturnValue(JSON.stringify(storedObject));
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useLocalStorage('user', { name: '', age: 0 })
|
||||
);
|
||||
|
||||
expect(result.current[0]).toEqual(storedObject);
|
||||
});
|
||||
|
||||
test('should handle arrays', () => {
|
||||
const storedArray = ['item1', 'item2', 'item3'];
|
||||
localStorageMock.getItem.mockReturnValue(JSON.stringify(storedArray));
|
||||
|
||||
const { result } = renderHook(() => useLocalStorage('items', []));
|
||||
|
||||
expect(result.current[0]).toEqual(storedArray);
|
||||
});
|
||||
|
||||
test('should handle boolean values', () => {
|
||||
localStorageMock.getItem.mockReturnValue(JSON.stringify(true));
|
||||
|
||||
const { result } = renderHook(() => useLocalStorage('isEnabled', false));
|
||||
|
||||
expect(result.current[0]).toBe(true);
|
||||
});
|
||||
|
||||
test('should handle number values', () => {
|
||||
localStorageMock.getItem.mockReturnValue(JSON.stringify(42));
|
||||
|
||||
const { result } = renderHook(() => useLocalStorage('count', 0));
|
||||
|
||||
expect(result.current[0]).toBe(42);
|
||||
});
|
||||
|
||||
test('should handle null values', () => {
|
||||
localStorageMock.getItem.mockReturnValue(JSON.stringify(null));
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useLocalStorage('nullable', 'default')
|
||||
);
|
||||
|
||||
expect(result.current[0]).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
test('should return default value when JSON parsing fails', () => {
|
||||
localStorageMock.getItem.mockReturnValue('invalid-json');
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useLocalStorage('test-key', 'default-value')
|
||||
);
|
||||
|
||||
expect(result.current[0]).toBe('default-value');
|
||||
});
|
||||
|
||||
test('should return default value when localStorage throws error', () => {
|
||||
localStorageMock.getItem.mockImplementation(() => {
|
||||
throw new Error('Storage error');
|
||||
});
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useLocalStorage('test-key', 'default-value')
|
||||
);
|
||||
|
||||
expect(result.current[0]).toBe('default-value');
|
||||
});
|
||||
|
||||
test('should handle corrupted data gracefully', () => {
|
||||
localStorageMock.getItem.mockReturnValue('{"incomplete": json');
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useLocalStorage('test-key', { complete: 'data' })
|
||||
);
|
||||
|
||||
expect(result.current[0]).toEqual({ complete: 'data' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('setValue functionality', () => {
|
||||
test('should update value and store in localStorage', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useLocalStorage('test-key', 'initial')
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current[1]('updated-value');
|
||||
});
|
||||
|
||||
expect(result.current[0]).toBe('updated-value');
|
||||
expect(localStorageMock.setItem).toHaveBeenCalledWith(
|
||||
'test-key',
|
||||
JSON.stringify('updated-value')
|
||||
);
|
||||
});
|
||||
|
||||
test('should handle function updates', () => {
|
||||
localStorageMock.getItem.mockReturnValue(JSON.stringify(5));
|
||||
|
||||
const { result } = renderHook(() => useLocalStorage('counter', 0));
|
||||
|
||||
act(() => {
|
||||
result.current[1]((prev: number) => prev + 1);
|
||||
});
|
||||
|
||||
expect(result.current[0]).toBe(6);
|
||||
expect(localStorageMock.setItem).toHaveBeenCalledWith(
|
||||
'counter',
|
||||
JSON.stringify(6)
|
||||
);
|
||||
});
|
||||
|
||||
test('should update complex objects', () => {
|
||||
const initialUser = { name: 'John', age: 30 };
|
||||
localStorageMock.getItem.mockReturnValue(JSON.stringify(initialUser));
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useLocalStorage('user', { name: '', age: 0 })
|
||||
);
|
||||
|
||||
const updatedUser = { name: 'Jane', age: 25 };
|
||||
|
||||
act(() => {
|
||||
result.current[1](updatedUser);
|
||||
});
|
||||
|
||||
expect(result.current[0]).toEqual(updatedUser);
|
||||
expect(localStorageMock.setItem).toHaveBeenCalledWith(
|
||||
'user',
|
||||
JSON.stringify(updatedUser)
|
||||
);
|
||||
});
|
||||
|
||||
test('should update arrays', () => {
|
||||
const initialItems = ['item1', 'item2'];
|
||||
localStorageMock.getItem.mockReturnValue(JSON.stringify(initialItems));
|
||||
|
||||
const { result } = renderHook(() => useLocalStorage('items', []));
|
||||
|
||||
act(() => {
|
||||
result.current[1]((prev: string[]) => [...prev, 'item3']);
|
||||
});
|
||||
|
||||
expect(result.current[0]).toEqual(['item1', 'item2', 'item3']);
|
||||
expect(localStorageMock.setItem).toHaveBeenCalledWith(
|
||||
'items',
|
||||
JSON.stringify(['item1', 'item2', 'item3'])
|
||||
);
|
||||
});
|
||||
|
||||
test('should handle setting null values', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useLocalStorage<string | null>('test-key', 'initial')
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current[1](null);
|
||||
});
|
||||
|
||||
expect(result.current[0]).toBe(null);
|
||||
expect(localStorageMock.setItem).toHaveBeenCalledWith(
|
||||
'test-key',
|
||||
JSON.stringify(null)
|
||||
);
|
||||
});
|
||||
|
||||
test('should handle setting undefined values', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useLocalStorage<string | undefined>('test-key', 'initial')
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current[1](undefined);
|
||||
});
|
||||
|
||||
expect(result.current[0]).toBe(undefined);
|
||||
expect(localStorageMock.setItem).toHaveBeenCalledWith(
|
||||
'test-key',
|
||||
JSON.stringify(undefined)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('server-side rendering (SSR) support', () => {
|
||||
test('should return default value when window is undefined', () => {
|
||||
const originalWindow = global.window;
|
||||
// @ts-ignore
|
||||
delete global.window;
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useLocalStorage('test-key', 'default-value')
|
||||
);
|
||||
|
||||
expect(result.current[0]).toBe('default-value');
|
||||
|
||||
// Restore window
|
||||
global.window = originalWindow;
|
||||
});
|
||||
});
|
||||
|
||||
describe('persistence across rerenders', () => {
|
||||
test('should maintain state across rerenders', () => {
|
||||
const { result, rerender } = renderHook(() =>
|
||||
useLocalStorage('test-key', 'initial')
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current[1]('updated');
|
||||
});
|
||||
|
||||
rerender();
|
||||
|
||||
expect(result.current[0]).toBe('updated');
|
||||
});
|
||||
|
||||
test('should sync with localStorage changes', () => {
|
||||
localStorageMock.getItem.mockReturnValue(JSON.stringify('stored-value'));
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useLocalStorage('test-key', 'default')
|
||||
);
|
||||
|
||||
expect(result.current[0]).toBe('stored-value');
|
||||
|
||||
// Simulate external localStorage change
|
||||
localStorageMock.getItem.mockReturnValue(
|
||||
JSON.stringify('externally-changed')
|
||||
);
|
||||
|
||||
// Value should remain the same until component re-mounts
|
||||
expect(result.current[0]).toBe('stored-value');
|
||||
});
|
||||
});
|
||||
|
||||
describe('type safety', () => {
|
||||
test('should maintain type safety with strings', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useLocalStorage('string-key', 'default')
|
||||
);
|
||||
|
||||
// TypeScript should enforce string type
|
||||
act(() => {
|
||||
result.current[1]('new-string');
|
||||
});
|
||||
|
||||
expect(typeof result.current[0]).toBe('string');
|
||||
});
|
||||
|
||||
test('should maintain type safety with numbers', () => {
|
||||
const { result } = renderHook(() => useLocalStorage('number-key', 42));
|
||||
|
||||
act(() => {
|
||||
result.current[1](100);
|
||||
});
|
||||
|
||||
expect(typeof result.current[0]).toBe('number');
|
||||
});
|
||||
|
||||
test('should maintain type safety with objects', () => {
|
||||
interface TestObject {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
const defaultObj: TestObject = { id: 1, name: 'test' };
|
||||
const { result } = renderHook(() =>
|
||||
useLocalStorage('object-key', defaultObj)
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current[1]({ id: 2, name: 'updated' });
|
||||
});
|
||||
|
||||
expect(result.current[0]).toHaveProperty('id');
|
||||
expect(result.current[0]).toHaveProperty('name');
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
test('should handle empty string key', () => {
|
||||
const { result } = renderHook(() => useLocalStorage('', 'default'));
|
||||
|
||||
expect(result.current[0]).toBe('default');
|
||||
expect(localStorageMock.getItem).toHaveBeenCalledWith('');
|
||||
});
|
||||
|
||||
test('should handle very long keys', () => {
|
||||
const longKey = 'a'.repeat(1000);
|
||||
const { result } = renderHook(() => useLocalStorage(longKey, 'default'));
|
||||
|
||||
expect(result.current[0]).toBe('default');
|
||||
expect(localStorageMock.getItem).toHaveBeenCalledWith(longKey);
|
||||
});
|
||||
|
||||
test('should handle special characters in key', () => {
|
||||
const specialKey = 'key-with-!@#$%^&*()_+-={}[]|\\:";\'<>?,./';
|
||||
const { result } = renderHook(() =>
|
||||
useLocalStorage(specialKey, 'default')
|
||||
);
|
||||
|
||||
expect(result.current[0]).toBe('default');
|
||||
expect(localStorageMock.getItem).toHaveBeenCalledWith(specialKey);
|
||||
});
|
||||
|
||||
test('should handle rapid state updates', () => {
|
||||
const { result } = renderHook(() => useLocalStorage('rapid-key', 0));
|
||||
|
||||
act(() => {
|
||||
for (let i = 1; i <= 10; i++) {
|
||||
result.current[1](i);
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.current[0]).toBe(10);
|
||||
expect(localStorageMock.setItem).toHaveBeenCalledTimes(10);
|
||||
});
|
||||
});
|
||||
|
||||
describe('memory cleanup', () => {
|
||||
test('should not leak memory on unmount', () => {
|
||||
const { unmount } = renderHook(() =>
|
||||
useLocalStorage('cleanup-key', 'default')
|
||||
);
|
||||
|
||||
unmount();
|
||||
|
||||
// Hook should not continue to interact with localStorage after unmount
|
||||
expect(localStorageMock.setItem).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
78
hooks/__tests__/useSettings.test.ts
Normal file
78
hooks/__tests__/useSettings.test.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import useSettings from '../useSettings';
|
||||
|
||||
// Mock fetch
|
||||
global.fetch = jest.fn();
|
||||
|
||||
describe('useSettings', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('should fetch settings on mount', async () => {
|
||||
const mockSettings = { theme: 'dark', notifications: true };
|
||||
(fetch as jest.MockedFunction<typeof fetch>).mockResolvedValue({
|
||||
ok: true,
|
||||
json: jest.fn().mockResolvedValue(mockSettings),
|
||||
} as any);
|
||||
|
||||
const { result } = renderHook(() => useSettings());
|
||||
|
||||
expect(result.current.loading).toBe(true);
|
||||
expect(result.current.settings).toBe(null);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
});
|
||||
|
||||
expect(result.current.settings).toEqual(mockSettings);
|
||||
expect(result.current.error).toBe(null);
|
||||
expect(fetch).toHaveBeenCalledWith('/api/settings');
|
||||
});
|
||||
|
||||
test('should handle fetch errors', async () => {
|
||||
const mockError = new Error('Network error');
|
||||
(fetch as jest.MockedFunction<typeof fetch>).mockRejectedValue(mockError);
|
||||
|
||||
const { result } = renderHook(() => useSettings());
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
});
|
||||
|
||||
expect(result.current.settings).toBe(null);
|
||||
expect(result.current.error).toBe(mockError);
|
||||
});
|
||||
|
||||
test('should update settings', async () => {
|
||||
const initialSettings = { theme: 'light', notifications: false };
|
||||
const updatedSettings = { theme: 'dark', notifications: true };
|
||||
|
||||
(fetch as jest.MockedFunction<typeof fetch>)
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: jest.fn().mockResolvedValue(initialSettings),
|
||||
} as any)
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: jest.fn().mockResolvedValue(updatedSettings),
|
||||
} as any);
|
||||
|
||||
const { result } = renderHook(() => useSettings());
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
});
|
||||
|
||||
await result.current.updateSettings(updatedSettings);
|
||||
|
||||
expect(result.current.settings).toEqual(updatedSettings);
|
||||
expect(fetch).toHaveBeenCalledWith('/api/settings', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(updatedSettings),
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user