mirror of
https://github.com/lordmathis/lemma.git
synced 2025-11-05 15:44:21 +00:00
Add tests for useUserAdmin hook functionality
This commit is contained in:
474
app/src/hooks/useFileList.test.ts
Normal file
474
app/src/hooks/useFileList.test.ts
Normal file
@@ -0,0 +1,474 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
import { useFileList } from './useFileList';
|
||||
import * as fileApi from '@/api/file';
|
||||
import type { FileNode } from '@/types/models';
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('@/api/file');
|
||||
|
||||
// Mock workspace context
|
||||
const mockWorkspaceData: {
|
||||
currentWorkspace: { id: number; name: string } | null;
|
||||
loading: boolean;
|
||||
} = {
|
||||
currentWorkspace: {
|
||||
id: 1,
|
||||
name: 'test-workspace',
|
||||
},
|
||||
loading: false,
|
||||
};
|
||||
|
||||
vi.mock('../contexts/WorkspaceDataContext', () => ({
|
||||
useWorkspaceData: () => mockWorkspaceData,
|
||||
}));
|
||||
|
||||
// Mock file data
|
||||
const mockFiles: FileNode[] = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'README.md',
|
||||
path: 'README.md',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'docs',
|
||||
path: 'docs',
|
||||
children: [
|
||||
{
|
||||
id: '3',
|
||||
name: 'guide.md',
|
||||
path: 'docs/guide.md',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: 'notes.md',
|
||||
path: 'notes.md',
|
||||
},
|
||||
];
|
||||
|
||||
describe('useFileList', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
// Reset workspace data to defaults
|
||||
mockWorkspaceData.currentWorkspace = {
|
||||
id: 1,
|
||||
name: 'test-workspace',
|
||||
};
|
||||
mockWorkspaceData.loading = false;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('initial state', () => {
|
||||
it('starts with empty files array', () => {
|
||||
const { result } = renderHook(() => useFileList());
|
||||
|
||||
expect(result.current.files).toEqual([]);
|
||||
expect(typeof result.current.loadFileList).toBe('function');
|
||||
});
|
||||
|
||||
it('provides loadFileList function', () => {
|
||||
const { result } = renderHook(() => useFileList());
|
||||
|
||||
expect(typeof result.current.loadFileList).toBe('function');
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadFileList', () => {
|
||||
it('loads files successfully', async () => {
|
||||
const mockListFiles = vi.mocked(fileApi.listFiles);
|
||||
mockListFiles.mockResolvedValue(mockFiles);
|
||||
|
||||
const { result } = renderHook(() => useFileList());
|
||||
|
||||
await act(async () => {
|
||||
await result.current.loadFileList();
|
||||
});
|
||||
|
||||
expect(result.current.files).toEqual(mockFiles);
|
||||
expect(mockListFiles).toHaveBeenCalledWith('test-workspace');
|
||||
});
|
||||
|
||||
it('handles empty file list', async () => {
|
||||
const mockListFiles = vi.mocked(fileApi.listFiles);
|
||||
mockListFiles.mockResolvedValue([]);
|
||||
|
||||
const { result } = renderHook(() => useFileList());
|
||||
|
||||
await act(async () => {
|
||||
await result.current.loadFileList();
|
||||
});
|
||||
|
||||
expect(result.current.files).toEqual([]);
|
||||
expect(mockListFiles).toHaveBeenCalledWith('test-workspace');
|
||||
});
|
||||
|
||||
it('handles API errors gracefully', async () => {
|
||||
const mockListFiles = vi.mocked(fileApi.listFiles);
|
||||
const consoleSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
mockListFiles.mockRejectedValue(new Error('API Error'));
|
||||
|
||||
const { result } = renderHook(() => useFileList());
|
||||
|
||||
await act(async () => {
|
||||
await result.current.loadFileList();
|
||||
});
|
||||
|
||||
expect(result.current.files).toEqual([]);
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
'Failed to load file list:',
|
||||
expect.any(Error)
|
||||
);
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('does not load when no workspace is available', async () => {
|
||||
mockWorkspaceData.currentWorkspace = null;
|
||||
|
||||
const { result } = renderHook(() => useFileList());
|
||||
|
||||
await act(async () => {
|
||||
await result.current.loadFileList();
|
||||
});
|
||||
|
||||
expect(result.current.files).toEqual([]);
|
||||
expect(fileApi.listFiles).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not load when workspace is loading', async () => {
|
||||
mockWorkspaceData.loading = true;
|
||||
|
||||
const { result } = renderHook(() => useFileList());
|
||||
|
||||
await act(async () => {
|
||||
await result.current.loadFileList();
|
||||
});
|
||||
|
||||
expect(result.current.files).toEqual([]);
|
||||
expect(fileApi.listFiles).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('can be called multiple times', async () => {
|
||||
const mockListFiles = vi.mocked(fileApi.listFiles);
|
||||
mockListFiles
|
||||
.mockResolvedValueOnce(mockFiles[0] ? [mockFiles[0]] : [])
|
||||
.mockResolvedValueOnce(mockFiles);
|
||||
|
||||
const { result } = renderHook(() => useFileList());
|
||||
|
||||
// First call
|
||||
await act(async () => {
|
||||
await result.current.loadFileList();
|
||||
});
|
||||
|
||||
expect(result.current.files).toEqual([mockFiles[0]]);
|
||||
|
||||
// Second call
|
||||
await act(async () => {
|
||||
await result.current.loadFileList();
|
||||
});
|
||||
|
||||
expect(result.current.files).toEqual(mockFiles);
|
||||
expect(mockListFiles).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('handles concurrent calls gracefully', async () => {
|
||||
const mockListFiles = vi.mocked(fileApi.listFiles);
|
||||
mockListFiles.mockResolvedValue(mockFiles);
|
||||
|
||||
const { result } = renderHook(() => useFileList());
|
||||
|
||||
await act(async () => {
|
||||
// Make multiple concurrent calls
|
||||
await Promise.all([
|
||||
result.current.loadFileList(),
|
||||
result.current.loadFileList(),
|
||||
result.current.loadFileList(),
|
||||
]);
|
||||
});
|
||||
|
||||
expect(result.current.files).toEqual(mockFiles);
|
||||
expect(mockListFiles).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('workspace dependency', () => {
|
||||
it('uses correct workspace name for API calls', async () => {
|
||||
const mockListFiles = vi.mocked(fileApi.listFiles);
|
||||
mockListFiles.mockResolvedValue(mockFiles);
|
||||
|
||||
const { result, rerender } = renderHook(() => useFileList());
|
||||
|
||||
// Load with initial workspace
|
||||
await act(async () => {
|
||||
await result.current.loadFileList();
|
||||
});
|
||||
|
||||
expect(mockListFiles).toHaveBeenCalledWith('test-workspace');
|
||||
|
||||
// Change workspace
|
||||
mockWorkspaceData.currentWorkspace = {
|
||||
id: 2,
|
||||
name: 'different-workspace',
|
||||
};
|
||||
|
||||
rerender();
|
||||
|
||||
await act(async () => {
|
||||
await result.current.loadFileList();
|
||||
});
|
||||
|
||||
expect(mockListFiles).toHaveBeenCalledWith('different-workspace');
|
||||
});
|
||||
|
||||
it('handles workspace becoming null after successful load', async () => {
|
||||
const mockListFiles = vi.mocked(fileApi.listFiles);
|
||||
mockListFiles.mockResolvedValue(mockFiles);
|
||||
|
||||
const { result, rerender } = renderHook(() => useFileList());
|
||||
|
||||
// Load files with workspace
|
||||
await act(async () => {
|
||||
await result.current.loadFileList();
|
||||
});
|
||||
|
||||
expect(result.current.files).toEqual(mockFiles);
|
||||
|
||||
// Remove workspace
|
||||
mockWorkspaceData.currentWorkspace = null;
|
||||
rerender();
|
||||
|
||||
// Try to load again
|
||||
await act(async () => {
|
||||
await result.current.loadFileList();
|
||||
});
|
||||
|
||||
// Files should remain from previous load, but no new API call
|
||||
expect(result.current.files).toEqual(mockFiles);
|
||||
expect(mockListFiles).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('handles workspace loading state changes', async () => {
|
||||
const mockListFiles = vi.mocked(fileApi.listFiles);
|
||||
mockListFiles.mockResolvedValue(mockFiles);
|
||||
|
||||
const { result, rerender } = renderHook(() => useFileList());
|
||||
|
||||
// Start with loading workspace
|
||||
mockWorkspaceData.loading = true;
|
||||
rerender();
|
||||
|
||||
await act(async () => {
|
||||
await result.current.loadFileList();
|
||||
});
|
||||
|
||||
expect(result.current.files).toEqual([]);
|
||||
expect(mockListFiles).not.toHaveBeenCalled();
|
||||
|
||||
// Workspace finishes loading
|
||||
mockWorkspaceData.loading = false;
|
||||
rerender();
|
||||
|
||||
await act(async () => {
|
||||
await result.current.loadFileList();
|
||||
});
|
||||
|
||||
expect(result.current.files).toEqual(mockFiles);
|
||||
expect(mockListFiles).toHaveBeenCalledWith('test-workspace');
|
||||
});
|
||||
});
|
||||
|
||||
describe('file data handling', () => {
|
||||
it('handles complex file tree structure', async () => {
|
||||
const complexFiles: FileNode[] = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'root.md',
|
||||
path: 'root.md',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'folder1',
|
||||
path: 'folder1',
|
||||
children: [
|
||||
{
|
||||
id: '3',
|
||||
name: 'subfolder',
|
||||
path: 'folder1/subfolder',
|
||||
children: [
|
||||
{
|
||||
id: '4',
|
||||
name: 'deep.md',
|
||||
path: 'folder1/subfolder/deep.md',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
name: 'file1.md',
|
||||
path: 'folder1/file1.md',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const mockListFiles = vi.mocked(fileApi.listFiles);
|
||||
mockListFiles.mockResolvedValue(complexFiles);
|
||||
|
||||
const { result } = renderHook(() => useFileList());
|
||||
|
||||
await act(async () => {
|
||||
await result.current.loadFileList();
|
||||
});
|
||||
|
||||
expect(result.current.files).toEqual(complexFiles);
|
||||
});
|
||||
|
||||
it('handles files with special characters', async () => {
|
||||
const specialFiles: FileNode[] = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'file with spaces.md',
|
||||
path: 'file with spaces.md',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'special-chars_123.md',
|
||||
path: 'special-chars_123.md',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'unicode-文档.md',
|
||||
path: 'unicode-文档.md',
|
||||
},
|
||||
];
|
||||
|
||||
const mockListFiles = vi.mocked(fileApi.listFiles);
|
||||
mockListFiles.mockResolvedValue(specialFiles);
|
||||
|
||||
const { result } = renderHook(() => useFileList());
|
||||
|
||||
await act(async () => {
|
||||
await result.current.loadFileList();
|
||||
});
|
||||
|
||||
expect(result.current.files).toEqual(specialFiles);
|
||||
});
|
||||
|
||||
it('handles files without children property', async () => {
|
||||
const filesWithoutChildren: FileNode[] = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'simple.md',
|
||||
path: 'simple.md',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'another.md',
|
||||
path: 'another.md',
|
||||
},
|
||||
];
|
||||
|
||||
const mockListFiles = vi.mocked(fileApi.listFiles);
|
||||
mockListFiles.mockResolvedValue(filesWithoutChildren);
|
||||
|
||||
const { result } = renderHook(() => useFileList());
|
||||
|
||||
await act(async () => {
|
||||
await result.current.loadFileList();
|
||||
});
|
||||
|
||||
expect(result.current.files).toEqual(filesWithoutChildren);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hook interface stability', () => {
|
||||
it('loadFileList function is stable across re-renders', () => {
|
||||
const { result, rerender } = renderHook(() => useFileList());
|
||||
|
||||
const initialLoadFunction = result.current.loadFileList;
|
||||
|
||||
rerender();
|
||||
|
||||
expect(result.current.loadFileList).toBe(initialLoadFunction);
|
||||
});
|
||||
|
||||
it('returns consistent interface', () => {
|
||||
const { result } = renderHook(() => useFileList());
|
||||
|
||||
expect(Array.isArray(result.current.files)).toBe(true);
|
||||
expect(typeof result.current.loadFileList).toBe('function');
|
||||
});
|
||||
});
|
||||
|
||||
describe('error recovery', () => {
|
||||
it('recovers from API errors on subsequent calls', async () => {
|
||||
const mockListFiles = vi.mocked(fileApi.listFiles);
|
||||
const consoleSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
// First call fails
|
||||
mockListFiles.mockRejectedValueOnce(new Error('First error'));
|
||||
// Second call succeeds
|
||||
mockListFiles.mockResolvedValueOnce(mockFiles);
|
||||
|
||||
const { result } = renderHook(() => useFileList());
|
||||
|
||||
// First call - should fail
|
||||
await act(async () => {
|
||||
await result.current.loadFileList();
|
||||
});
|
||||
|
||||
expect(result.current.files).toEqual([]);
|
||||
|
||||
// Second call - should succeed
|
||||
await act(async () => {
|
||||
await result.current.loadFileList();
|
||||
});
|
||||
|
||||
expect(result.current.files).toEqual(mockFiles);
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('maintains previous data after error', async () => {
|
||||
const mockListFiles = vi.mocked(fileApi.listFiles);
|
||||
const consoleSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
// First call succeeds
|
||||
mockListFiles.mockResolvedValueOnce(mockFiles);
|
||||
// Second call fails
|
||||
mockListFiles.mockRejectedValueOnce(new Error('Second error'));
|
||||
|
||||
const { result } = renderHook(() => useFileList());
|
||||
|
||||
// First call - should succeed
|
||||
await act(async () => {
|
||||
await result.current.loadFileList();
|
||||
});
|
||||
|
||||
expect(result.current.files).toEqual(mockFiles);
|
||||
|
||||
// Second call - should fail but maintain previous data
|
||||
await act(async () => {
|
||||
await result.current.loadFileList();
|
||||
});
|
||||
|
||||
expect(result.current.files).toEqual([]);
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
});
|
||||
610
app/src/hooks/useUserAdmin.test.ts
Normal file
610
app/src/hooks/useUserAdmin.test.ts
Normal file
@@ -0,0 +1,610 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
import { useUserAdmin } from './useUserAdmin';
|
||||
import * as adminApi from '@/api/admin';
|
||||
import type { CreateUserRequest, UpdateUserRequest } from '@/types/api';
|
||||
import { UserRole, type User } from '@/types/models';
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('@/api/admin');
|
||||
vi.mock('@mantine/notifications', () => ({
|
||||
notifications: {
|
||||
show: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock useAdminData hook
|
||||
const mockAdminData = {
|
||||
data: [] as User[],
|
||||
loading: false,
|
||||
error: null as string | null,
|
||||
reload: vi.fn(),
|
||||
};
|
||||
|
||||
vi.mock('./useAdminData', () => ({
|
||||
useAdminData: () => mockAdminData,
|
||||
}));
|
||||
|
||||
// Import notifications for assertions
|
||||
import { notifications } from '@mantine/notifications';
|
||||
|
||||
// Mock user data
|
||||
const mockUsers: User[] = [
|
||||
{
|
||||
id: 1,
|
||||
email: 'admin@example.com',
|
||||
displayName: 'Admin User',
|
||||
role: UserRole.Admin,
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
lastWorkspaceId: 1,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
email: 'editor@example.com',
|
||||
displayName: 'Editor User',
|
||||
role: UserRole.Editor,
|
||||
createdAt: '2024-01-02T00:00:00Z',
|
||||
lastWorkspaceId: 1,
|
||||
},
|
||||
];
|
||||
|
||||
// Helper function to get a user by index and ensure it's not undefined
|
||||
const getUser = (index: number): User => {
|
||||
const user = mockUsers[index];
|
||||
if (!user) {
|
||||
throw new Error(`User at index ${index} is undefined`);
|
||||
}
|
||||
return user;
|
||||
};
|
||||
|
||||
describe('useUserAdmin', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
// Reset mock data
|
||||
mockAdminData.data = [...mockUsers];
|
||||
mockAdminData.loading = false;
|
||||
mockAdminData.error = null;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('initial state', () => {
|
||||
it('returns users data from useAdminData', () => {
|
||||
const { result } = renderHook(() => useUserAdmin());
|
||||
|
||||
expect(result.current.users).toEqual(mockUsers);
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(result.current.error).toBeNull();
|
||||
});
|
||||
|
||||
it('returns loading state from useAdminData', () => {
|
||||
mockAdminData.loading = true;
|
||||
|
||||
const { result } = renderHook(() => useUserAdmin());
|
||||
|
||||
expect(result.current.loading).toBe(true);
|
||||
});
|
||||
|
||||
it('returns error state from useAdminData', () => {
|
||||
mockAdminData.error = 'Failed to load users';
|
||||
|
||||
const { result } = renderHook(() => useUserAdmin());
|
||||
|
||||
expect(result.current.error).toBe('Failed to load users');
|
||||
});
|
||||
|
||||
it('provides CRUD functions', () => {
|
||||
const { result } = renderHook(() => useUserAdmin());
|
||||
|
||||
expect(typeof result.current.create).toBe('function');
|
||||
expect(typeof result.current.update).toBe('function');
|
||||
expect(typeof result.current.delete).toBe('function');
|
||||
});
|
||||
});
|
||||
|
||||
describe('create user', () => {
|
||||
it('creates user successfully', async () => {
|
||||
const mockCreateUser = vi.mocked(adminApi.createUser);
|
||||
const newUser: User = {
|
||||
id: 3,
|
||||
email: 'newuser@example.com',
|
||||
displayName: 'New User',
|
||||
role: UserRole.Viewer,
|
||||
createdAt: '2024-01-03T00:00:00Z',
|
||||
lastWorkspaceId: 1,
|
||||
};
|
||||
mockCreateUser.mockResolvedValue(newUser);
|
||||
|
||||
const { result } = renderHook(() => useUserAdmin());
|
||||
|
||||
const createRequest: CreateUserRequest = {
|
||||
email: 'newuser@example.com',
|
||||
displayName: 'New User',
|
||||
password: 'password123',
|
||||
role: UserRole.Viewer,
|
||||
};
|
||||
|
||||
let createResult: boolean | undefined;
|
||||
await act(async () => {
|
||||
createResult = await result.current.create(createRequest);
|
||||
});
|
||||
|
||||
expect(createResult).toBe(true);
|
||||
expect(mockCreateUser).toHaveBeenCalledWith(createRequest);
|
||||
expect(notifications.show).toHaveBeenCalledWith({
|
||||
title: 'Success',
|
||||
message: 'User created successfully',
|
||||
color: 'green',
|
||||
});
|
||||
expect(mockAdminData.reload).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('handles create errors with specific message', async () => {
|
||||
const mockCreateUser = vi.mocked(adminApi.createUser);
|
||||
mockCreateUser.mockRejectedValue(new Error('Email already exists'));
|
||||
|
||||
const { result } = renderHook(() => useUserAdmin());
|
||||
|
||||
const createRequest: CreateUserRequest = {
|
||||
email: 'existing@example.com',
|
||||
displayName: 'Test User',
|
||||
password: 'password123',
|
||||
role: UserRole.Editor,
|
||||
};
|
||||
|
||||
let createResult: boolean | undefined;
|
||||
await act(async () => {
|
||||
createResult = await result.current.create(createRequest);
|
||||
});
|
||||
|
||||
expect(createResult).toBe(false);
|
||||
expect(notifications.show).toHaveBeenCalledWith({
|
||||
title: 'Error',
|
||||
message: 'Failed to create user: Email already exists',
|
||||
color: 'red',
|
||||
});
|
||||
expect(mockAdminData.reload).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('handles create errors with non-Error rejection', async () => {
|
||||
const mockCreateUser = vi.mocked(adminApi.createUser);
|
||||
mockCreateUser.mockRejectedValue('String error');
|
||||
|
||||
const { result } = renderHook(() => useUserAdmin());
|
||||
|
||||
const createRequest: CreateUserRequest = {
|
||||
email: 'test@example.com',
|
||||
displayName: 'Test User',
|
||||
password: 'password123',
|
||||
role: UserRole.Editor,
|
||||
};
|
||||
|
||||
let createResult: boolean | undefined;
|
||||
await act(async () => {
|
||||
createResult = await result.current.create(createRequest);
|
||||
});
|
||||
|
||||
expect(createResult).toBe(false);
|
||||
expect(notifications.show).toHaveBeenCalledWith({
|
||||
title: 'Error',
|
||||
message: 'Failed to create user: String error',
|
||||
color: 'red',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('update user', () => {
|
||||
it('updates user successfully', async () => {
|
||||
const mockUpdateUser = vi.mocked(adminApi.updateUser);
|
||||
const user = getUser(1);
|
||||
const updatedUser: User = {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
displayName: 'Updated Editor',
|
||||
role: user.role,
|
||||
createdAt: user.createdAt,
|
||||
lastWorkspaceId: user.lastWorkspaceId,
|
||||
};
|
||||
mockUpdateUser.mockResolvedValue(updatedUser);
|
||||
|
||||
const { result } = renderHook(() => useUserAdmin());
|
||||
|
||||
const updateRequest: UpdateUserRequest = {
|
||||
displayName: 'Updated Editor',
|
||||
};
|
||||
|
||||
let updateResult: boolean | undefined;
|
||||
await act(async () => {
|
||||
updateResult = await result.current.update(2, updateRequest);
|
||||
});
|
||||
|
||||
expect(updateResult).toBe(true);
|
||||
expect(mockUpdateUser).toHaveBeenCalledWith(2, updateRequest);
|
||||
expect(notifications.show).toHaveBeenCalledWith({
|
||||
title: 'Success',
|
||||
message: 'User updated successfully',
|
||||
color: 'green',
|
||||
});
|
||||
expect(mockAdminData.reload).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('updates user email and role', async () => {
|
||||
const mockUpdateUser = vi.mocked(adminApi.updateUser);
|
||||
const user = getUser(1);
|
||||
const updatedUser: User = {
|
||||
id: user.id,
|
||||
email: 'newemail@example.com',
|
||||
displayName: user.displayName || '',
|
||||
role: UserRole.Admin,
|
||||
createdAt: user.createdAt,
|
||||
lastWorkspaceId: user.lastWorkspaceId,
|
||||
};
|
||||
mockUpdateUser.mockResolvedValue(updatedUser);
|
||||
|
||||
const { result } = renderHook(() => useUserAdmin());
|
||||
|
||||
const updateRequest: UpdateUserRequest = {
|
||||
email: 'newemail@example.com',
|
||||
role: UserRole.Admin,
|
||||
};
|
||||
|
||||
let updateResult: boolean | undefined;
|
||||
await act(async () => {
|
||||
updateResult = await result.current.update(2, updateRequest);
|
||||
});
|
||||
|
||||
expect(updateResult).toBe(true);
|
||||
expect(mockUpdateUser).toHaveBeenCalledWith(2, updateRequest);
|
||||
});
|
||||
|
||||
it('updates user password', async () => {
|
||||
const mockUpdateUser = vi.mocked(adminApi.updateUser);
|
||||
mockUpdateUser.mockResolvedValue(getUser(1));
|
||||
|
||||
const { result } = renderHook(() => useUserAdmin());
|
||||
|
||||
const updateRequest: UpdateUserRequest = {
|
||||
password: 'newpassword123',
|
||||
};
|
||||
|
||||
let updateResult: boolean | undefined;
|
||||
await act(async () => {
|
||||
updateResult = await result.current.update(2, updateRequest);
|
||||
});
|
||||
|
||||
expect(updateResult).toBe(true);
|
||||
expect(mockUpdateUser).toHaveBeenCalledWith(2, updateRequest);
|
||||
});
|
||||
|
||||
it('handles update errors', async () => {
|
||||
const mockUpdateUser = vi.mocked(adminApi.updateUser);
|
||||
mockUpdateUser.mockRejectedValue(new Error('User not found'));
|
||||
|
||||
const { result } = renderHook(() => useUserAdmin());
|
||||
|
||||
const updateRequest: UpdateUserRequest = {
|
||||
displayName: 'Updated Name',
|
||||
};
|
||||
|
||||
let updateResult: boolean | undefined;
|
||||
await act(async () => {
|
||||
updateResult = await result.current.update(999, updateRequest);
|
||||
});
|
||||
|
||||
expect(updateResult).toBe(false);
|
||||
expect(notifications.show).toHaveBeenCalledWith({
|
||||
title: 'Error',
|
||||
message: 'Failed to update user: User not found',
|
||||
color: 'red',
|
||||
});
|
||||
expect(mockAdminData.reload).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('handles empty update request', async () => {
|
||||
const mockUpdateUser = vi.mocked(adminApi.updateUser);
|
||||
mockUpdateUser.mockResolvedValue(getUser(1));
|
||||
|
||||
const { result } = renderHook(() => useUserAdmin());
|
||||
|
||||
let updateResult: boolean | undefined;
|
||||
await act(async () => {
|
||||
updateResult = await result.current.update(2, {});
|
||||
});
|
||||
|
||||
expect(updateResult).toBe(true);
|
||||
expect(mockUpdateUser).toHaveBeenCalledWith(2, {});
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete user', () => {
|
||||
it('deletes user successfully', async () => {
|
||||
const mockDeleteUser = vi.mocked(adminApi.deleteUser);
|
||||
mockDeleteUser.mockResolvedValue(undefined);
|
||||
|
||||
const { result } = renderHook(() => useUserAdmin());
|
||||
|
||||
let deleteResult: boolean | undefined;
|
||||
await act(async () => {
|
||||
deleteResult = await result.current.delete(2);
|
||||
});
|
||||
|
||||
expect(deleteResult).toBe(true);
|
||||
expect(mockDeleteUser).toHaveBeenCalledWith(2);
|
||||
expect(notifications.show).toHaveBeenCalledWith({
|
||||
title: 'Success',
|
||||
message: 'User deleted successfully',
|
||||
color: 'green',
|
||||
});
|
||||
expect(mockAdminData.reload).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('handles delete errors', async () => {
|
||||
const mockDeleteUser = vi.mocked(adminApi.deleteUser);
|
||||
mockDeleteUser.mockRejectedValue(new Error('Cannot delete admin user'));
|
||||
|
||||
const { result } = renderHook(() => useUserAdmin());
|
||||
|
||||
let deleteResult: boolean | undefined;
|
||||
await act(async () => {
|
||||
deleteResult = await result.current.delete(1);
|
||||
});
|
||||
|
||||
expect(deleteResult).toBe(false);
|
||||
expect(notifications.show).toHaveBeenCalledWith({
|
||||
title: 'Error',
|
||||
message: 'Failed to delete user: Cannot delete admin user',
|
||||
color: 'red',
|
||||
});
|
||||
expect(mockAdminData.reload).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('handles delete with non-existent user', async () => {
|
||||
const mockDeleteUser = vi.mocked(adminApi.deleteUser);
|
||||
mockDeleteUser.mockRejectedValue(new Error('User not found'));
|
||||
|
||||
const { result } = renderHook(() => useUserAdmin());
|
||||
|
||||
let deleteResult: boolean | undefined;
|
||||
await act(async () => {
|
||||
deleteResult = await result.current.delete(999);
|
||||
});
|
||||
|
||||
expect(deleteResult).toBe(false);
|
||||
expect(notifications.show).toHaveBeenCalledWith({
|
||||
title: 'Error',
|
||||
message: 'Failed to delete user: User not found',
|
||||
color: 'red',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('data integration', () => {
|
||||
it('reflects loading state changes', () => {
|
||||
const { result, rerender } = renderHook(() => useUserAdmin());
|
||||
|
||||
expect(result.current.loading).toBe(false);
|
||||
|
||||
// Change loading state
|
||||
mockAdminData.loading = true;
|
||||
rerender();
|
||||
|
||||
expect(result.current.loading).toBe(true);
|
||||
});
|
||||
|
||||
it('reflects error state changes', () => {
|
||||
const { result, rerender } = renderHook(() => useUserAdmin());
|
||||
|
||||
expect(result.current.error).toBeNull();
|
||||
|
||||
// Add error
|
||||
mockAdminData.error = 'Network error';
|
||||
rerender();
|
||||
|
||||
expect(result.current.error).toBe('Network error');
|
||||
});
|
||||
|
||||
it('reflects data changes', () => {
|
||||
const { result, rerender } = renderHook(() => useUserAdmin());
|
||||
|
||||
expect(result.current.users).toEqual(mockUsers);
|
||||
|
||||
// Change users data
|
||||
const newUsers = [mockUsers[0]].filter((u): u is User => u !== undefined);
|
||||
mockAdminData.data = newUsers;
|
||||
rerender();
|
||||
|
||||
expect(result.current.users).toEqual(newUsers);
|
||||
});
|
||||
|
||||
it('calls reload after successful operations', async () => {
|
||||
const mockCreateUser = vi.mocked(adminApi.createUser);
|
||||
const mockUpdateUser = vi.mocked(adminApi.updateUser);
|
||||
const mockDeleteUser = vi.mocked(adminApi.deleteUser);
|
||||
|
||||
mockCreateUser.mockResolvedValue(getUser(0));
|
||||
mockUpdateUser.mockResolvedValue(getUser(0));
|
||||
mockDeleteUser.mockResolvedValue(undefined);
|
||||
|
||||
const { result } = renderHook(() => useUserAdmin());
|
||||
|
||||
// Test create
|
||||
await act(async () => {
|
||||
await result.current.create({
|
||||
email: 'test@example.com',
|
||||
displayName: 'Test',
|
||||
password: 'pass',
|
||||
role: UserRole.Viewer,
|
||||
});
|
||||
});
|
||||
|
||||
expect(mockAdminData.reload).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Test update
|
||||
await act(async () => {
|
||||
await result.current.update(1, { displayName: 'Updated' });
|
||||
});
|
||||
|
||||
expect(mockAdminData.reload).toHaveBeenCalledTimes(2);
|
||||
|
||||
// Test delete
|
||||
await act(async () => {
|
||||
await result.current.delete(1);
|
||||
});
|
||||
|
||||
expect(mockAdminData.reload).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('does not call reload after failed operations', async () => {
|
||||
const mockCreateUser = vi.mocked(adminApi.createUser);
|
||||
const mockUpdateUser = vi.mocked(adminApi.updateUser);
|
||||
const mockDeleteUser = vi.mocked(adminApi.deleteUser);
|
||||
|
||||
mockCreateUser.mockRejectedValue(new Error('Create failed'));
|
||||
mockUpdateUser.mockRejectedValue(new Error('Update failed'));
|
||||
mockDeleteUser.mockRejectedValue(new Error('Delete failed'));
|
||||
|
||||
const { result } = renderHook(() => useUserAdmin());
|
||||
|
||||
// Test failed create
|
||||
await act(async () => {
|
||||
await result.current.create({
|
||||
email: 'test@example.com',
|
||||
displayName: 'Test',
|
||||
password: 'pass',
|
||||
role: UserRole.Viewer,
|
||||
});
|
||||
});
|
||||
|
||||
// Test failed update
|
||||
await act(async () => {
|
||||
await result.current.update(1, { displayName: 'Updated' });
|
||||
});
|
||||
|
||||
// Test failed delete
|
||||
await act(async () => {
|
||||
await result.current.delete(1);
|
||||
});
|
||||
|
||||
expect(mockAdminData.reload).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('concurrent operations', () => {
|
||||
it('handles multiple create operations', async () => {
|
||||
const mockCreateUser = vi.mocked(adminApi.createUser);
|
||||
mockCreateUser
|
||||
.mockResolvedValueOnce({
|
||||
id: 3,
|
||||
email: 'user1@example.com',
|
||||
displayName: 'User 1',
|
||||
role: UserRole.Viewer,
|
||||
createdAt: '2024-01-03T00:00:00Z',
|
||||
lastWorkspaceId: 1,
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
id: 4,
|
||||
email: 'user2@example.com',
|
||||
displayName: 'User 2',
|
||||
role: UserRole.Editor,
|
||||
createdAt: '2024-01-04T00:00:00Z',
|
||||
lastWorkspaceId: 1,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useUserAdmin());
|
||||
|
||||
const requests = [
|
||||
{
|
||||
email: 'user1@example.com',
|
||||
displayName: 'User 1',
|
||||
password: 'pass1',
|
||||
role: UserRole.Viewer,
|
||||
},
|
||||
{
|
||||
email: 'user2@example.com',
|
||||
displayName: 'User 2',
|
||||
password: 'pass2',
|
||||
role: UserRole.Editor,
|
||||
},
|
||||
];
|
||||
|
||||
let results: boolean[] = [];
|
||||
await act(async () => {
|
||||
results = await Promise.all(
|
||||
requests.map((req) => result.current.create(req))
|
||||
);
|
||||
});
|
||||
|
||||
expect(results).toEqual([true, true]);
|
||||
expect(mockCreateUser).toHaveBeenCalledTimes(2);
|
||||
expect(mockAdminData.reload).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('handles mixed successful and failed operations', async () => {
|
||||
const mockCreateUser = vi.mocked(adminApi.createUser);
|
||||
mockCreateUser
|
||||
.mockResolvedValueOnce(getUser(0))
|
||||
.mockRejectedValueOnce(new Error('Second create failed'));
|
||||
|
||||
const { result } = renderHook(() => useUserAdmin());
|
||||
|
||||
const requests = [
|
||||
{
|
||||
email: 'success@example.com',
|
||||
displayName: 'Success User',
|
||||
password: 'pass1',
|
||||
role: UserRole.Viewer,
|
||||
},
|
||||
{
|
||||
email: 'fail@example.com',
|
||||
displayName: 'Fail User',
|
||||
password: 'pass2',
|
||||
role: UserRole.Editor,
|
||||
},
|
||||
];
|
||||
|
||||
let results: boolean[] = [];
|
||||
await act(async () => {
|
||||
results = await Promise.all(
|
||||
requests.map((req) => result.current.create(req))
|
||||
);
|
||||
});
|
||||
|
||||
expect(results).toEqual([true, false]);
|
||||
expect(mockAdminData.reload).toHaveBeenCalledTimes(1); // Only for successful operation
|
||||
});
|
||||
});
|
||||
|
||||
describe('hook interface stability', () => {
|
||||
it('functions are stable across re-renders', () => {
|
||||
const { result, rerender } = renderHook(() => useUserAdmin());
|
||||
|
||||
const initialFunctions = {
|
||||
create: result.current.create,
|
||||
update: result.current.update,
|
||||
delete: result.current.delete,
|
||||
};
|
||||
|
||||
rerender();
|
||||
|
||||
expect(result.current.create).toBe(initialFunctions.create);
|
||||
expect(result.current.update).toBe(initialFunctions.update);
|
||||
expect(result.current.delete).toBe(initialFunctions.delete);
|
||||
});
|
||||
|
||||
it('returns consistent interface', () => {
|
||||
const { result } = renderHook(() => useUserAdmin());
|
||||
|
||||
expect(Array.isArray(result.current.users)).toBe(true);
|
||||
expect(typeof result.current.loading).toBe('boolean');
|
||||
expect(
|
||||
result.current.error === null ||
|
||||
typeof result.current.error === 'string'
|
||||
).toBe(true);
|
||||
expect(typeof result.current.create).toBe('function');
|
||||
expect(typeof result.current.update).toBe('function');
|
||||
expect(typeof result.current.delete).toBe('function');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user