mirror of
https://github.com/lordmathis/lemma.git
synced 2025-11-05 23:44:22 +00:00
Refactor admin API types and add validation functions for WorkspaceStats and FileCountStats
This commit is contained in:
@@ -7,10 +7,10 @@ import { apiCall } from './api';
|
|||||||
import {
|
import {
|
||||||
isSystemStats,
|
isSystemStats,
|
||||||
isUser,
|
isUser,
|
||||||
isWorkspace,
|
isWorkspaceStats,
|
||||||
type SystemStats,
|
type SystemStats,
|
||||||
type User,
|
type User,
|
||||||
type Workspace,
|
type WorkspaceStats,
|
||||||
} from '@/types/models';
|
} from '@/types/models';
|
||||||
|
|
||||||
const ADMIN_BASE_URL = `${API_BASE_URL}/admin`;
|
const ADMIN_BASE_URL = `${API_BASE_URL}/admin`;
|
||||||
@@ -101,18 +101,18 @@ export const updateUser = async (
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches all workspaces from the API
|
* Fetches all workspaces from the API
|
||||||
* @returns {Promise<Workspace[]>} A promise that resolves to an array of workspaces
|
* @returns {Promise<WorkspaceStats[]>} A promise that resolves to an array of workspaces
|
||||||
* @throws {Error} If the API call fails or returns an invalid response
|
* @throws {Error} If the API call fails or returns an invalid response
|
||||||
* */
|
* */
|
||||||
export const getWorkspaces = async (): Promise<Workspace[]> => {
|
export const getWorkspaces = async (): Promise<WorkspaceStats[]> => {
|
||||||
const response = await apiCall(`${ADMIN_BASE_URL}/workspaces`);
|
const response = await apiCall(`${ADMIN_BASE_URL}/workspaces`);
|
||||||
const data: unknown = await response.json();
|
const data: unknown = await response.json();
|
||||||
if (!Array.isArray(data)) {
|
if (!Array.isArray(data)) {
|
||||||
throw new Error('Invalid workspaces response received from API');
|
throw new Error('Invalid workspaces response received from API');
|
||||||
}
|
}
|
||||||
return data.map((workspace) => {
|
return data.map((workspace) => {
|
||||||
if (!isWorkspace(workspace)) {
|
if (!isWorkspaceStats(workspace)) {
|
||||||
throw new Error('Invalid workspace object received from API');
|
throw new Error('Invalid workspace stats object received from API');
|
||||||
}
|
}
|
||||||
return workspace;
|
return workspace;
|
||||||
});
|
});
|
||||||
|
|||||||
608
app/src/hooks/useAdminData.test.ts
Normal file
608
app/src/hooks/useAdminData.test.ts
Normal file
@@ -0,0 +1,608 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||||
|
import { renderHook, act, waitFor } from '@testing-library/react';
|
||||||
|
import { useAdminData } from './useAdminData';
|
||||||
|
import * as adminApi from '@/api/admin';
|
||||||
|
import {
|
||||||
|
UserRole,
|
||||||
|
type SystemStats,
|
||||||
|
type User,
|
||||||
|
type WorkspaceStats,
|
||||||
|
} from '@/types/models';
|
||||||
|
|
||||||
|
// Mock dependencies
|
||||||
|
vi.mock('@/api/admin');
|
||||||
|
vi.mock('@mantine/notifications', () => ({
|
||||||
|
notifications: {
|
||||||
|
show: vi.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Import notifications for assertions
|
||||||
|
import { notifications } from '@mantine/notifications';
|
||||||
|
|
||||||
|
// Mock data
|
||||||
|
const mockSystemStats: SystemStats = {
|
||||||
|
totalUsers: 10,
|
||||||
|
activeUsers: 8,
|
||||||
|
totalWorkspaces: 15,
|
||||||
|
totalFiles: 150,
|
||||||
|
totalSize: 1024000,
|
||||||
|
};
|
||||||
|
|
||||||
|
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: 2,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const mockWorkspaceStats: WorkspaceStats[] = [
|
||||||
|
{
|
||||||
|
userID: 1,
|
||||||
|
userEmail: 'admin@example.com',
|
||||||
|
workspaceID: 1,
|
||||||
|
workspaceName: 'admin-workspace',
|
||||||
|
workspaceCreatedAt: '2024-01-01T00:00:00Z',
|
||||||
|
fileCountStats: {
|
||||||
|
totalFiles: 10,
|
||||||
|
totalSize: 204800,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
userID: 2,
|
||||||
|
userEmail: 'editor@example.com',
|
||||||
|
workspaceID: 2,
|
||||||
|
workspaceName: 'editor-workspace',
|
||||||
|
workspaceCreatedAt: '2024-01-02T00:00:00Z',
|
||||||
|
fileCountStats: {
|
||||||
|
totalFiles: 15,
|
||||||
|
totalSize: 307200,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
describe('useAdminData', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('stats data type', () => {
|
||||||
|
it('initializes with empty stats and loading state', () => {
|
||||||
|
const { result } = renderHook(() => useAdminData('stats'));
|
||||||
|
|
||||||
|
expect(result.current.data).toEqual({});
|
||||||
|
expect(result.current.loading).toBe(true);
|
||||||
|
expect(result.current.error).toBeNull();
|
||||||
|
expect(typeof result.current.reload).toBe('function');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads system stats successfully', async () => {
|
||||||
|
const mockGetSystemStats = vi.mocked(adminApi.getSystemStats);
|
||||||
|
mockGetSystemStats.mockResolvedValue(mockSystemStats);
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useAdminData('stats'));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.loading).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.data).toEqual(mockSystemStats);
|
||||||
|
expect(result.current.error).toBeNull();
|
||||||
|
expect(mockGetSystemStats).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles stats loading errors', async () => {
|
||||||
|
const mockGetSystemStats = vi.mocked(adminApi.getSystemStats);
|
||||||
|
mockGetSystemStats.mockRejectedValue(new Error('Failed to load stats'));
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useAdminData('stats'));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.loading).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.data).toEqual({});
|
||||||
|
expect(result.current.error).toBe('Failed to load stats');
|
||||||
|
expect(notifications.show).toHaveBeenCalledWith({
|
||||||
|
title: 'Error',
|
||||||
|
message: 'Failed to load stats: Failed to load stats',
|
||||||
|
color: 'red',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reloads stats data', async () => {
|
||||||
|
const mockGetSystemStats = vi.mocked(adminApi.getSystemStats);
|
||||||
|
mockGetSystemStats.mockResolvedValue(mockSystemStats);
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useAdminData('stats'));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.loading).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockGetSystemStats).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.reload();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockGetSystemStats).toHaveBeenCalledTimes(2);
|
||||||
|
expect(result.current.data).toEqual(mockSystemStats);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('users data type', () => {
|
||||||
|
it('initializes with empty users array and loading state', () => {
|
||||||
|
const { result } = renderHook(() => useAdminData('users'));
|
||||||
|
|
||||||
|
expect(result.current.data).toEqual([]);
|
||||||
|
expect(result.current.loading).toBe(true);
|
||||||
|
expect(result.current.error).toBeNull();
|
||||||
|
expect(typeof result.current.reload).toBe('function');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads users successfully', async () => {
|
||||||
|
const mockGetUsers = vi.mocked(adminApi.getUsers);
|
||||||
|
mockGetUsers.mockResolvedValue(mockUsers);
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useAdminData('users'));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.loading).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.data).toEqual(mockUsers);
|
||||||
|
expect(result.current.error).toBeNull();
|
||||||
|
expect(mockGetUsers).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles users loading errors', async () => {
|
||||||
|
const mockGetUsers = vi.mocked(adminApi.getUsers);
|
||||||
|
mockGetUsers.mockRejectedValue(new Error('Failed to load users'));
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useAdminData('users'));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.loading).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.data).toEqual([]);
|
||||||
|
expect(result.current.error).toBe('Failed to load users');
|
||||||
|
expect(notifications.show).toHaveBeenCalledWith({
|
||||||
|
title: 'Error',
|
||||||
|
message: 'Failed to load users: Failed to load users',
|
||||||
|
color: 'red',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reloads users data', async () => {
|
||||||
|
const mockGetUsers = vi.mocked(adminApi.getUsers);
|
||||||
|
mockGetUsers.mockResolvedValue(mockUsers);
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useAdminData('users'));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.loading).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockGetUsers).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.reload();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockGetUsers).toHaveBeenCalledTimes(2);
|
||||||
|
expect(result.current.data).toEqual(mockUsers);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles empty users array', async () => {
|
||||||
|
const mockGetUsers = vi.mocked(adminApi.getUsers);
|
||||||
|
mockGetUsers.mockResolvedValue([]);
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useAdminData('users'));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.loading).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.data).toEqual([]);
|
||||||
|
expect(result.current.error).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('workspaces data type', () => {
|
||||||
|
it('initializes with empty workspaces array and loading state', () => {
|
||||||
|
const { result } = renderHook(() => useAdminData('workspaces'));
|
||||||
|
|
||||||
|
expect(result.current.data).toEqual([]);
|
||||||
|
expect(result.current.loading).toBe(true);
|
||||||
|
expect(result.current.error).toBeNull();
|
||||||
|
expect(typeof result.current.reload).toBe('function');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads workspaces successfully', async () => {
|
||||||
|
const mockGetWorkspaces = vi.mocked(adminApi.getWorkspaces);
|
||||||
|
mockGetWorkspaces.mockResolvedValue(mockWorkspaceStats);
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useAdminData('workspaces'));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.loading).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.data).toEqual(mockWorkspaceStats);
|
||||||
|
expect(result.current.error).toBeNull();
|
||||||
|
expect(mockGetWorkspaces).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles workspaces loading errors', async () => {
|
||||||
|
const mockGetWorkspaces = vi.mocked(adminApi.getWorkspaces);
|
||||||
|
mockGetWorkspaces.mockRejectedValue(
|
||||||
|
new Error('Failed to load workspaces')
|
||||||
|
);
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useAdminData('workspaces'));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.loading).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.data).toEqual([]);
|
||||||
|
expect(result.current.error).toBe('Failed to load workspaces');
|
||||||
|
expect(notifications.show).toHaveBeenCalledWith({
|
||||||
|
title: 'Error',
|
||||||
|
message: 'Failed to load workspaces: Failed to load workspaces',
|
||||||
|
color: 'red',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reloads workspaces data', async () => {
|
||||||
|
const mockGetWorkspaces = vi.mocked(adminApi.getWorkspaces);
|
||||||
|
mockGetWorkspaces.mockResolvedValue(mockWorkspaceStats);
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useAdminData('workspaces'));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.loading).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockGetWorkspaces).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.reload();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockGetWorkspaces).toHaveBeenCalledTimes(2);
|
||||||
|
expect(result.current.data).toEqual(mockWorkspaceStats);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles workspaces with minimal configuration', async () => {
|
||||||
|
const minimalWorkspaceStats: WorkspaceStats[] = [
|
||||||
|
{
|
||||||
|
userID: 3,
|
||||||
|
userEmail: 'minimal@example.com',
|
||||||
|
workspaceID: 3,
|
||||||
|
workspaceName: 'minimal-workspace',
|
||||||
|
workspaceCreatedAt: '2024-01-03T00:00:00Z',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const mockGetWorkspaces = vi.mocked(adminApi.getWorkspaces);
|
||||||
|
mockGetWorkspaces.mockResolvedValue(minimalWorkspaceStats);
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useAdminData('workspaces'));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.loading).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.data).toEqual(minimalWorkspaceStats);
|
||||||
|
expect(result.current.error).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('error handling', () => {
|
||||||
|
it('handles API errors with error response object', async () => {
|
||||||
|
const mockGetSystemStats = vi.mocked(adminApi.getSystemStats);
|
||||||
|
// Create a properly typed error object to simulate API error response
|
||||||
|
const errorWithResponse = new Error('Request failed');
|
||||||
|
type ErrorWithResponse = Error & {
|
||||||
|
response: {
|
||||||
|
data: {
|
||||||
|
error: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
(errorWithResponse as ErrorWithResponse).response = {
|
||||||
|
data: {
|
||||||
|
error: 'Custom API error message',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
mockGetSystemStats.mockRejectedValue(errorWithResponse);
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useAdminData('stats'));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.loading).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.error).toBe('Custom API error message');
|
||||||
|
expect(notifications.show).toHaveBeenCalledWith({
|
||||||
|
title: 'Error',
|
||||||
|
message: 'Failed to load stats: Custom API error message',
|
||||||
|
color: 'red',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles unknown errors gracefully', async () => {
|
||||||
|
const mockGetUsers = vi.mocked(adminApi.getUsers);
|
||||||
|
mockGetUsers.mockRejectedValue('String error');
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useAdminData('users'));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.loading).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.error).toBe('An unknown error occurred');
|
||||||
|
expect(notifications.show).toHaveBeenCalledWith({
|
||||||
|
title: 'Error',
|
||||||
|
message: 'Failed to load users: An unknown error occurred',
|
||||||
|
color: 'red',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles network timeout errors', async () => {
|
||||||
|
const mockGetWorkspaces = vi.mocked(adminApi.getWorkspaces);
|
||||||
|
mockGetWorkspaces.mockRejectedValue(new Error('Network timeout'));
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useAdminData('workspaces'));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.loading).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.error).toBe('Network timeout');
|
||||||
|
expect(notifications.show).toHaveBeenCalledWith({
|
||||||
|
title: 'Error',
|
||||||
|
message: 'Failed to load workspaces: Network timeout',
|
||||||
|
color: 'red',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clears error on successful reload', async () => {
|
||||||
|
const mockGetSystemStats = vi.mocked(adminApi.getSystemStats);
|
||||||
|
mockGetSystemStats
|
||||||
|
.mockRejectedValueOnce(new Error('Initial error'))
|
||||||
|
.mockResolvedValueOnce(mockSystemStats);
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useAdminData('stats'));
|
||||||
|
|
||||||
|
// Wait for initial error
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.error).toBe('Initial error');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reload successfully
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.reload();
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.error).toBeNull();
|
||||||
|
expect(result.current.data).toEqual(mockSystemStats);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('loading state management', () => {
|
||||||
|
it('manages loading state correctly during initial load', async () => {
|
||||||
|
const mockGetSystemStats = vi.mocked(adminApi.getSystemStats);
|
||||||
|
let resolvePromise: (value: SystemStats) => void;
|
||||||
|
const pendingPromise = new Promise<SystemStats>((resolve) => {
|
||||||
|
resolvePromise = resolve;
|
||||||
|
});
|
||||||
|
mockGetSystemStats.mockReturnValue(pendingPromise);
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useAdminData('stats'));
|
||||||
|
|
||||||
|
expect(result.current.loading).toBe(true);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
resolvePromise!(mockSystemStats);
|
||||||
|
await pendingPromise;
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.loading).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('manages loading state correctly during reload', async () => {
|
||||||
|
const mockGetUsers = vi.mocked(adminApi.getUsers);
|
||||||
|
mockGetUsers.mockResolvedValue(mockUsers);
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useAdminData('users'));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.loading).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
let resolveReload: (value: User[]) => void;
|
||||||
|
const reloadPromise = new Promise<User[]>((resolve) => {
|
||||||
|
resolveReload = resolve;
|
||||||
|
});
|
||||||
|
mockGetUsers.mockReturnValueOnce(reloadPromise);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
void result.current.reload();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.loading).toBe(true);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
resolveReload!(mockUsers);
|
||||||
|
await reloadPromise;
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.loading).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles loading state during error scenarios', async () => {
|
||||||
|
const mockGetWorkspaces = vi.mocked(adminApi.getWorkspaces);
|
||||||
|
let rejectPromise: (error: Error) => void;
|
||||||
|
const errorPromise = new Promise<WorkspaceStats[]>((_, reject) => {
|
||||||
|
rejectPromise = reject;
|
||||||
|
});
|
||||||
|
mockGetWorkspaces.mockReturnValue(errorPromise);
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useAdminData('workspaces'));
|
||||||
|
|
||||||
|
expect(result.current.loading).toBe(true);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
rejectPromise!(new Error('Load failed'));
|
||||||
|
try {
|
||||||
|
await errorPromise;
|
||||||
|
} catch {
|
||||||
|
// Expected to fail
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.loading).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('data consistency', () => {
|
||||||
|
it('maintains data consistency across re-renders', async () => {
|
||||||
|
const mockGetSystemStats = vi.mocked(adminApi.getSystemStats);
|
||||||
|
mockGetSystemStats.mockResolvedValue(mockSystemStats);
|
||||||
|
|
||||||
|
const { result, rerender } = renderHook(() => useAdminData('stats'));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.loading).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
const initialData = result.current.data;
|
||||||
|
|
||||||
|
rerender();
|
||||||
|
|
||||||
|
expect(result.current.data).toBe(initialData);
|
||||||
|
expect(result.current.data).toEqual(mockSystemStats);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('provides stable reload function across re-renders', () => {
|
||||||
|
const { result, rerender } = renderHook(() => useAdminData('stats'));
|
||||||
|
|
||||||
|
const initialReload = result.current.reload;
|
||||||
|
|
||||||
|
rerender();
|
||||||
|
|
||||||
|
expect(result.current.reload).toBe(initialReload);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles data type changes correctly', () => {
|
||||||
|
const { result: statsResult } = renderHook(() => useAdminData('stats'));
|
||||||
|
const { result: usersResult } = renderHook(() => useAdminData('users'));
|
||||||
|
const { result: workspacesResult } = renderHook(() =>
|
||||||
|
useAdminData('workspaces')
|
||||||
|
);
|
||||||
|
|
||||||
|
// Different data types should have different initial values
|
||||||
|
expect(statsResult.current.data).toEqual({});
|
||||||
|
expect(usersResult.current.data).toEqual([]);
|
||||||
|
expect(workspacesResult.current.data).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('function stability', () => {
|
||||||
|
it('maintains stable reload function reference', () => {
|
||||||
|
const { result, rerender } = renderHook(() => useAdminData('stats'));
|
||||||
|
|
||||||
|
const initialReload = result.current.reload;
|
||||||
|
|
||||||
|
rerender();
|
||||||
|
|
||||||
|
expect(result.current.reload).toBe(initialReload);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('concurrent operations', () => {
|
||||||
|
it('handles multiple concurrent reloads', async () => {
|
||||||
|
const mockGetSystemStats = vi.mocked(adminApi.getSystemStats);
|
||||||
|
mockGetSystemStats.mockResolvedValue(mockSystemStats);
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useAdminData('stats'));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.loading).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Trigger multiple reloads
|
||||||
|
await act(async () => {
|
||||||
|
await Promise.all([
|
||||||
|
result.current.reload(),
|
||||||
|
result.current.reload(),
|
||||||
|
result.current.reload(),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockGetSystemStats).toHaveBeenCalledTimes(4); // 1 initial + 3 reloads
|
||||||
|
expect(result.current.data).toEqual(mockSystemStats);
|
||||||
|
expect(result.current.loading).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('edge cases', () => {
|
||||||
|
it('handles invalid data type gracefully', async () => {
|
||||||
|
// This would normally be caught by TypeScript, but test runtime behavior
|
||||||
|
const mockGetSystemStats = vi.mocked(adminApi.getSystemStats);
|
||||||
|
mockGetSystemStats.mockRejectedValue(new Error('Invalid data type'));
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useAdminData('stats'));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.loading).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.error).toBe('Invalid data type');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles partial data responses', async () => {
|
||||||
|
const partialStats = {
|
||||||
|
totalUsers: 5,
|
||||||
|
activeUsers: 3,
|
||||||
|
// Missing other required fields
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockGetSystemStats = vi.mocked(adminApi.getSystemStats);
|
||||||
|
mockGetSystemStats.mockResolvedValue(partialStats as SystemStats);
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useAdminData('stats'));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.loading).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.data).toEqual(partialStats);
|
||||||
|
expect(result.current.error).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -208,12 +208,46 @@ export interface WorkspaceStats {
|
|||||||
fileCountStats?: FileCountStats;
|
fileCountStats?: FileCountStats;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isWorkspaceStats checks if the given object is a valid WorkspaceStats object
|
||||||
|
export function isWorkspaceStats(obj: unknown): obj is WorkspaceStats {
|
||||||
|
return (
|
||||||
|
typeof obj === 'object' &&
|
||||||
|
obj !== null &&
|
||||||
|
'userID' in obj &&
|
||||||
|
typeof (obj as WorkspaceStats).userID === 'number' &&
|
||||||
|
'userEmail' in obj &&
|
||||||
|
typeof (obj as WorkspaceStats).userEmail === 'string' &&
|
||||||
|
'workspaceID' in obj &&
|
||||||
|
typeof (obj as WorkspaceStats).workspaceID === 'number' &&
|
||||||
|
'workspaceName' in obj &&
|
||||||
|
typeof (obj as WorkspaceStats).workspaceName === 'string' &&
|
||||||
|
'workspaceCreatedAt' in obj &&
|
||||||
|
typeof (obj as WorkspaceStats).workspaceCreatedAt === 'string' &&
|
||||||
|
(!('fileCountStats' in obj) ||
|
||||||
|
(obj as WorkspaceStats).fileCountStats === undefined ||
|
||||||
|
(obj as WorkspaceStats).fileCountStats === null ||
|
||||||
|
isFileCountStats((obj as WorkspaceStats).fileCountStats))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Define FileCountStats based on the Go struct definition of storage.FileCountStats
|
// Define FileCountStats based on the Go struct definition of storage.FileCountStats
|
||||||
export interface FileCountStats {
|
export interface FileCountStats {
|
||||||
totalFiles: number;
|
totalFiles: number;
|
||||||
totalSize: number;
|
totalSize: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isFileCountStats checks if the given object is a valid FileCountStats object
|
||||||
|
export function isFileCountStats(obj: unknown): obj is FileCountStats {
|
||||||
|
return (
|
||||||
|
typeof obj === 'object' &&
|
||||||
|
obj !== null &&
|
||||||
|
'totalFiles' in obj &&
|
||||||
|
typeof (obj as FileCountStats).totalFiles === 'number' &&
|
||||||
|
'totalSize' in obj &&
|
||||||
|
typeof (obj as FileCountStats).totalSize === 'number'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export interface UserStats {
|
export interface UserStats {
|
||||||
totalUsers: number;
|
totalUsers: number;
|
||||||
totalWorkspaces: number;
|
totalWorkspaces: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user