- 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>
582 lines
17 KiB
TypeScript
582 lines
17 KiB
TypeScript
/**
|
|
* 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);
|
|
});
|
|
});
|
|
});
|
|
});
|