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:
581
frontend/src/contexts/__tests__/SandboxContext.test.tsx
Normal file
581
frontend/src/contexts/__tests__/SandboxContext.test.tsx
Normal file
@@ -0,0 +1,581 @@
|
||||
/**
|
||||
* Unit tests for SandboxContext
|
||||
*
|
||||
* Tests the sandbox context provider and hook including:
|
||||
* - Default values when used outside provider
|
||||
* - Providing sandbox status from hooks
|
||||
* - Toggle functionality
|
||||
* - Loading and pending states
|
||||
* - localStorage synchronization
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
|
||||
// Mock the sandbox hooks
|
||||
vi.mock('../../hooks/useSandbox', () => ({
|
||||
useSandboxStatus: vi.fn(),
|
||||
useToggleSandbox: vi.fn(),
|
||||
}));
|
||||
|
||||
import { SandboxProvider, useSandbox } from '../SandboxContext';
|
||||
import { useSandboxStatus, useToggleSandbox } from '../../hooks/useSandbox';
|
||||
|
||||
// Mock localStorage
|
||||
const localStorageMock = (() => {
|
||||
let store: Record<string, string> = {};
|
||||
|
||||
return {
|
||||
getItem: (key: string) => store[key] || null,
|
||||
setItem: (key: string, value: string) => {
|
||||
store[key] = value.toString();
|
||||
},
|
||||
removeItem: (key: string) => {
|
||||
delete store[key];
|
||||
},
|
||||
clear: () => {
|
||||
store = {};
|
||||
},
|
||||
};
|
||||
})();
|
||||
|
||||
Object.defineProperty(window, 'localStorage', {
|
||||
value: localStorageMock,
|
||||
});
|
||||
|
||||
// Test wrapper with QueryClient
|
||||
const createWrapper = (queryClient: QueryClient) => {
|
||||
return ({ children }: { children: React.ReactNode }) => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<SandboxProvider>{children}</SandboxProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
|
||||
describe('SandboxContext', () => {
|
||||
let queryClient: QueryClient;
|
||||
|
||||
beforeEach(() => {
|
||||
queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false, gcTime: 0 },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
});
|
||||
vi.clearAllMocks();
|
||||
localStorageMock.clear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
queryClient.clear();
|
||||
localStorageMock.clear();
|
||||
});
|
||||
|
||||
describe('useSandbox hook', () => {
|
||||
it('should return default values when used outside provider', () => {
|
||||
const { result } = renderHook(() => useSandbox());
|
||||
|
||||
expect(result.current).toEqual({
|
||||
isSandbox: false,
|
||||
sandboxEnabled: false,
|
||||
isLoading: false,
|
||||
toggleSandbox: expect.any(Function),
|
||||
isToggling: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow calling toggleSandbox without error when outside provider', async () => {
|
||||
const { result } = renderHook(() => useSandbox());
|
||||
|
||||
// Should not throw an error
|
||||
await expect(result.current.toggleSandbox(true)).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('SandboxProvider', () => {
|
||||
describe('sandbox status', () => {
|
||||
it('should provide sandbox status from hook when sandbox is disabled', async () => {
|
||||
const mockStatusData = {
|
||||
sandbox_mode: false,
|
||||
sandbox_enabled: false,
|
||||
};
|
||||
|
||||
vi.mocked(useSandboxStatus).mockReturnValue({
|
||||
data: mockStatusData,
|
||||
isLoading: false,
|
||||
isSuccess: true,
|
||||
isError: false,
|
||||
error: null,
|
||||
} as any);
|
||||
|
||||
vi.mocked(useToggleSandbox).mockReturnValue({
|
||||
mutateAsync: vi.fn(),
|
||||
isPending: false,
|
||||
} as any);
|
||||
|
||||
const { result } = renderHook(() => useSandbox(), {
|
||||
wrapper: createWrapper(queryClient),
|
||||
});
|
||||
|
||||
expect(result.current.isSandbox).toBe(false);
|
||||
expect(result.current.sandboxEnabled).toBe(false);
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
it('should provide sandbox status when sandbox is enabled and active', async () => {
|
||||
const mockStatusData = {
|
||||
sandbox_mode: true,
|
||||
sandbox_enabled: true,
|
||||
};
|
||||
|
||||
vi.mocked(useSandboxStatus).mockReturnValue({
|
||||
data: mockStatusData,
|
||||
isLoading: false,
|
||||
isSuccess: true,
|
||||
isError: false,
|
||||
error: null,
|
||||
} as any);
|
||||
|
||||
vi.mocked(useToggleSandbox).mockReturnValue({
|
||||
mutateAsync: vi.fn(),
|
||||
isPending: false,
|
||||
} as any);
|
||||
|
||||
const { result } = renderHook(() => useSandbox(), {
|
||||
wrapper: createWrapper(queryClient),
|
||||
});
|
||||
|
||||
expect(result.current.isSandbox).toBe(true);
|
||||
expect(result.current.sandboxEnabled).toBe(true);
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
it('should provide sandbox status when sandbox is enabled but not active', async () => {
|
||||
const mockStatusData = {
|
||||
sandbox_mode: false,
|
||||
sandbox_enabled: true,
|
||||
};
|
||||
|
||||
vi.mocked(useSandboxStatus).mockReturnValue({
|
||||
data: mockStatusData,
|
||||
isLoading: false,
|
||||
isSuccess: true,
|
||||
isError: false,
|
||||
error: null,
|
||||
} as any);
|
||||
|
||||
vi.mocked(useToggleSandbox).mockReturnValue({
|
||||
mutateAsync: vi.fn(),
|
||||
isPending: false,
|
||||
} as any);
|
||||
|
||||
const { result } = renderHook(() => useSandbox(), {
|
||||
wrapper: createWrapper(queryClient),
|
||||
});
|
||||
|
||||
expect(result.current.isSandbox).toBe(false);
|
||||
expect(result.current.sandboxEnabled).toBe(true);
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle loading state', () => {
|
||||
vi.mocked(useSandboxStatus).mockReturnValue({
|
||||
data: undefined,
|
||||
isLoading: true,
|
||||
isSuccess: false,
|
||||
isError: false,
|
||||
error: null,
|
||||
} as any);
|
||||
|
||||
vi.mocked(useToggleSandbox).mockReturnValue({
|
||||
mutateAsync: vi.fn(),
|
||||
isPending: false,
|
||||
} as any);
|
||||
|
||||
const { result } = renderHook(() => useSandbox(), {
|
||||
wrapper: createWrapper(queryClient),
|
||||
});
|
||||
|
||||
expect(result.current.isLoading).toBe(true);
|
||||
expect(result.current.isSandbox).toBe(false);
|
||||
expect(result.current.sandboxEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it('should default to false when data is undefined', () => {
|
||||
vi.mocked(useSandboxStatus).mockReturnValue({
|
||||
data: undefined,
|
||||
isLoading: false,
|
||||
isSuccess: false,
|
||||
isError: false,
|
||||
error: null,
|
||||
} as any);
|
||||
|
||||
vi.mocked(useToggleSandbox).mockReturnValue({
|
||||
mutateAsync: vi.fn(),
|
||||
isPending: false,
|
||||
} as any);
|
||||
|
||||
const { result } = renderHook(() => useSandbox(), {
|
||||
wrapper: createWrapper(queryClient),
|
||||
});
|
||||
|
||||
expect(result.current.isSandbox).toBe(false);
|
||||
expect(result.current.sandboxEnabled).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toggleSandbox function', () => {
|
||||
it('should provide toggleSandbox function that calls mutation', async () => {
|
||||
const mockMutateAsync = vi.fn().mockResolvedValue({ sandbox_mode: true });
|
||||
|
||||
vi.mocked(useSandboxStatus).mockReturnValue({
|
||||
data: { sandbox_mode: false, sandbox_enabled: true },
|
||||
isLoading: false,
|
||||
isSuccess: true,
|
||||
isError: false,
|
||||
error: null,
|
||||
} as any);
|
||||
|
||||
vi.mocked(useToggleSandbox).mockReturnValue({
|
||||
mutateAsync: mockMutateAsync,
|
||||
isPending: false,
|
||||
} as any);
|
||||
|
||||
const { result } = renderHook(() => useSandbox(), {
|
||||
wrapper: createWrapper(queryClient),
|
||||
});
|
||||
|
||||
await result.current.toggleSandbox(true);
|
||||
|
||||
expect(mockMutateAsync).toHaveBeenCalledWith(true);
|
||||
expect(mockMutateAsync).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should call mutation with false to disable sandbox', async () => {
|
||||
const mockMutateAsync = vi.fn().mockResolvedValue({ sandbox_mode: false });
|
||||
|
||||
vi.mocked(useSandboxStatus).mockReturnValue({
|
||||
data: { sandbox_mode: true, sandbox_enabled: true },
|
||||
isLoading: false,
|
||||
isSuccess: true,
|
||||
isError: false,
|
||||
error: null,
|
||||
} as any);
|
||||
|
||||
vi.mocked(useToggleSandbox).mockReturnValue({
|
||||
mutateAsync: mockMutateAsync,
|
||||
isPending: false,
|
||||
} as any);
|
||||
|
||||
const { result } = renderHook(() => useSandbox(), {
|
||||
wrapper: createWrapper(queryClient),
|
||||
});
|
||||
|
||||
await result.current.toggleSandbox(false);
|
||||
|
||||
expect(mockMutateAsync).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it('should propagate errors from mutation', async () => {
|
||||
const mockError = new Error('Failed to toggle sandbox');
|
||||
const mockMutateAsync = vi.fn().mockRejectedValue(mockError);
|
||||
|
||||
vi.mocked(useSandboxStatus).mockReturnValue({
|
||||
data: { sandbox_mode: false, sandbox_enabled: true },
|
||||
isLoading: false,
|
||||
isSuccess: true,
|
||||
isError: false,
|
||||
error: null,
|
||||
} as any);
|
||||
|
||||
vi.mocked(useToggleSandbox).mockReturnValue({
|
||||
mutateAsync: mockMutateAsync,
|
||||
isPending: false,
|
||||
} as any);
|
||||
|
||||
const { result } = renderHook(() => useSandbox(), {
|
||||
wrapper: createWrapper(queryClient),
|
||||
});
|
||||
|
||||
await expect(result.current.toggleSandbox(true)).rejects.toThrow('Failed to toggle sandbox');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isToggling state', () => {
|
||||
it('should reflect mutation pending state as false', () => {
|
||||
vi.mocked(useSandboxStatus).mockReturnValue({
|
||||
data: { sandbox_mode: false, sandbox_enabled: true },
|
||||
isLoading: false,
|
||||
isSuccess: true,
|
||||
isError: false,
|
||||
error: null,
|
||||
} as any);
|
||||
|
||||
vi.mocked(useToggleSandbox).mockReturnValue({
|
||||
mutateAsync: vi.fn(),
|
||||
isPending: false,
|
||||
} as any);
|
||||
|
||||
const { result } = renderHook(() => useSandbox(), {
|
||||
wrapper: createWrapper(queryClient),
|
||||
});
|
||||
|
||||
expect(result.current.isToggling).toBe(false);
|
||||
});
|
||||
|
||||
it('should reflect mutation pending state as true', () => {
|
||||
vi.mocked(useSandboxStatus).mockReturnValue({
|
||||
data: { sandbox_mode: false, sandbox_enabled: true },
|
||||
isLoading: false,
|
||||
isSuccess: true,
|
||||
isError: false,
|
||||
error: null,
|
||||
} as any);
|
||||
|
||||
vi.mocked(useToggleSandbox).mockReturnValue({
|
||||
mutateAsync: vi.fn(),
|
||||
isPending: true,
|
||||
} as any);
|
||||
|
||||
const { result } = renderHook(() => useSandbox(), {
|
||||
wrapper: createWrapper(queryClient),
|
||||
});
|
||||
|
||||
expect(result.current.isToggling).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('localStorage synchronization', () => {
|
||||
beforeEach(() => {
|
||||
localStorageMock.clear();
|
||||
});
|
||||
|
||||
it('should update localStorage when sandbox_mode is true', async () => {
|
||||
vi.mocked(useSandboxStatus).mockReturnValue({
|
||||
data: { sandbox_mode: true, sandbox_enabled: true },
|
||||
isLoading: false,
|
||||
isSuccess: true,
|
||||
isError: false,
|
||||
error: null,
|
||||
} as any);
|
||||
|
||||
vi.mocked(useToggleSandbox).mockReturnValue({
|
||||
mutateAsync: vi.fn(),
|
||||
isPending: false,
|
||||
} as any);
|
||||
|
||||
renderHook(() => useSandbox(), {
|
||||
wrapper: createWrapper(queryClient),
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(localStorageMock.getItem('sandbox_mode')).toBe('true');
|
||||
});
|
||||
});
|
||||
|
||||
it('should update localStorage when sandbox_mode is false', async () => {
|
||||
vi.mocked(useSandboxStatus).mockReturnValue({
|
||||
data: { sandbox_mode: false, sandbox_enabled: true },
|
||||
isLoading: false,
|
||||
isSuccess: true,
|
||||
isError: false,
|
||||
error: null,
|
||||
} as any);
|
||||
|
||||
vi.mocked(useToggleSandbox).mockReturnValue({
|
||||
mutateAsync: vi.fn(),
|
||||
isPending: false,
|
||||
} as any);
|
||||
|
||||
renderHook(() => useSandbox(), {
|
||||
wrapper: createWrapper(queryClient),
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(localStorageMock.getItem('sandbox_mode')).toBe('false');
|
||||
});
|
||||
});
|
||||
|
||||
it('should update localStorage when status changes from false to true', async () => {
|
||||
// First render with sandbox_mode = false
|
||||
vi.mocked(useSandboxStatus).mockReturnValue({
|
||||
data: { sandbox_mode: false, sandbox_enabled: true },
|
||||
isLoading: false,
|
||||
isSuccess: true,
|
||||
isError: false,
|
||||
error: null,
|
||||
} as any);
|
||||
|
||||
vi.mocked(useToggleSandbox).mockReturnValue({
|
||||
mutateAsync: vi.fn(),
|
||||
isPending: false,
|
||||
} as any);
|
||||
|
||||
const { unmount } = renderHook(() => useSandbox(), {
|
||||
wrapper: createWrapper(queryClient),
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(localStorageMock.getItem('sandbox_mode')).toBe('false');
|
||||
});
|
||||
|
||||
unmount();
|
||||
|
||||
// Re-render with sandbox_mode = true
|
||||
vi.mocked(useSandboxStatus).mockReturnValue({
|
||||
data: { sandbox_mode: true, sandbox_enabled: true },
|
||||
isLoading: false,
|
||||
isSuccess: true,
|
||||
isError: false,
|
||||
error: null,
|
||||
} as any);
|
||||
|
||||
renderHook(() => useSandbox(), {
|
||||
wrapper: createWrapper(queryClient),
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(localStorageMock.getItem('sandbox_mode')).toBe('true');
|
||||
});
|
||||
});
|
||||
|
||||
it('should not update localStorage when sandbox_mode is undefined', async () => {
|
||||
vi.mocked(useSandboxStatus).mockReturnValue({
|
||||
data: undefined,
|
||||
isLoading: true,
|
||||
isSuccess: false,
|
||||
isError: false,
|
||||
error: null,
|
||||
} as any);
|
||||
|
||||
vi.mocked(useToggleSandbox).mockReturnValue({
|
||||
mutateAsync: vi.fn(),
|
||||
isPending: false,
|
||||
} as any);
|
||||
|
||||
renderHook(() => useSandbox(), {
|
||||
wrapper: createWrapper(queryClient),
|
||||
});
|
||||
|
||||
// Wait a bit to ensure effect had time to run
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
expect(localStorageMock.getItem('sandbox_mode')).toBeNull();
|
||||
});
|
||||
|
||||
it('should not update localStorage when status data is partial', async () => {
|
||||
vi.mocked(useSandboxStatus).mockReturnValue({
|
||||
data: { sandbox_enabled: true } as any,
|
||||
isLoading: false,
|
||||
isSuccess: true,
|
||||
isError: false,
|
||||
error: null,
|
||||
} as any);
|
||||
|
||||
vi.mocked(useToggleSandbox).mockReturnValue({
|
||||
mutateAsync: vi.fn(),
|
||||
isPending: false,
|
||||
} as any);
|
||||
|
||||
renderHook(() => useSandbox(), {
|
||||
wrapper: createWrapper(queryClient),
|
||||
});
|
||||
|
||||
// Wait a bit to ensure effect had time to run
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
expect(localStorageMock.getItem('sandbox_mode')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('integration scenarios', () => {
|
||||
it('should handle complete toggle workflow', async () => {
|
||||
const mockMutateAsync = vi.fn().mockResolvedValue({ sandbox_mode: true });
|
||||
|
||||
vi.mocked(useSandboxStatus).mockReturnValue({
|
||||
data: { sandbox_mode: false, sandbox_enabled: true },
|
||||
isLoading: false,
|
||||
isSuccess: true,
|
||||
isError: false,
|
||||
error: null,
|
||||
} as any);
|
||||
|
||||
vi.mocked(useToggleSandbox).mockReturnValue({
|
||||
mutateAsync: mockMutateAsync,
|
||||
isPending: false,
|
||||
} as any);
|
||||
|
||||
const { result } = renderHook(() => useSandbox(), {
|
||||
wrapper: createWrapper(queryClient),
|
||||
});
|
||||
|
||||
// Initial state
|
||||
expect(result.current.isSandbox).toBe(false);
|
||||
expect(result.current.isToggling).toBe(false);
|
||||
expect(localStorageMock.getItem('sandbox_mode')).toBe('false');
|
||||
|
||||
// Toggle sandbox
|
||||
await result.current.toggleSandbox(true);
|
||||
|
||||
expect(mockMutateAsync).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it('should handle disabled sandbox feature', () => {
|
||||
vi.mocked(useSandboxStatus).mockReturnValue({
|
||||
data: { sandbox_mode: false, sandbox_enabled: false },
|
||||
isLoading: false,
|
||||
isSuccess: true,
|
||||
isError: false,
|
||||
error: null,
|
||||
} as any);
|
||||
|
||||
vi.mocked(useToggleSandbox).mockReturnValue({
|
||||
mutateAsync: vi.fn(),
|
||||
isPending: false,
|
||||
} as any);
|
||||
|
||||
const { result } = renderHook(() => useSandbox(), {
|
||||
wrapper: createWrapper(queryClient),
|
||||
});
|
||||
|
||||
expect(result.current.isSandbox).toBe(false);
|
||||
expect(result.current.sandboxEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle multiple rapid toggle calls', async () => {
|
||||
const mockMutateAsync = vi.fn()
|
||||
.mockResolvedValueOnce({ sandbox_mode: true })
|
||||
.mockResolvedValueOnce({ sandbox_mode: false })
|
||||
.mockResolvedValueOnce({ sandbox_mode: true });
|
||||
|
||||
vi.mocked(useSandboxStatus).mockReturnValue({
|
||||
data: { sandbox_mode: false, sandbox_enabled: true },
|
||||
isLoading: false,
|
||||
isSuccess: true,
|
||||
isError: false,
|
||||
error: null,
|
||||
} as any);
|
||||
|
||||
vi.mocked(useToggleSandbox).mockReturnValue({
|
||||
mutateAsync: mockMutateAsync,
|
||||
isPending: false,
|
||||
} as any);
|
||||
|
||||
const { result } = renderHook(() => useSandbox(), {
|
||||
wrapper: createWrapper(queryClient),
|
||||
});
|
||||
|
||||
// Multiple rapid calls
|
||||
await Promise.all([
|
||||
result.current.toggleSandbox(true),
|
||||
result.current.toggleSandbox(false),
|
||||
result.current.toggleSandbox(true),
|
||||
]);
|
||||
|
||||
expect(mockMutateAsync).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user