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:
579
frontend/src/hooks/__tests__/useSandbox.test.ts
Normal file
579
frontend/src/hooks/__tests__/useSandbox.test.ts
Normal file
@@ -0,0 +1,579 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { renderHook, waitFor, act } from '@testing-library/react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import React from 'react';
|
||||
|
||||
// Mock the sandbox API
|
||||
vi.mock('../../api/sandbox', () => ({
|
||||
getSandboxStatus: vi.fn(),
|
||||
toggleSandboxMode: vi.fn(),
|
||||
resetSandboxData: vi.fn(),
|
||||
}));
|
||||
|
||||
import {
|
||||
useSandboxStatus,
|
||||
useToggleSandbox,
|
||||
useResetSandbox,
|
||||
} from '../useSandbox';
|
||||
import * as sandboxApi from '../../api/sandbox';
|
||||
|
||||
// Create wrapper
|
||||
const createWrapper = () => {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
});
|
||||
|
||||
return function Wrapper({ children }: { children: React.ReactNode }) {
|
||||
return React.createElement(QueryClientProvider, { client: queryClient }, children);
|
||||
};
|
||||
};
|
||||
|
||||
describe('useSandbox hooks', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('useSandboxStatus', () => {
|
||||
it('fetches sandbox status', async () => {
|
||||
const mockStatus = {
|
||||
sandbox_mode: true,
|
||||
sandbox_enabled: true,
|
||||
sandbox_schema: 'business_1_sandbox',
|
||||
};
|
||||
vi.mocked(sandboxApi.getSandboxStatus).mockResolvedValue(mockStatus);
|
||||
|
||||
const { result } = renderHook(() => useSandboxStatus(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isSuccess).toBe(true);
|
||||
});
|
||||
|
||||
expect(sandboxApi.getSandboxStatus).toHaveBeenCalled();
|
||||
expect(result.current.data).toEqual(mockStatus);
|
||||
});
|
||||
|
||||
it('returns sandbox_mode as false when in live mode', async () => {
|
||||
const mockStatus = {
|
||||
sandbox_mode: false,
|
||||
sandbox_enabled: true,
|
||||
sandbox_schema: 'business_1_sandbox',
|
||||
};
|
||||
vi.mocked(sandboxApi.getSandboxStatus).mockResolvedValue(mockStatus);
|
||||
|
||||
const { result } = renderHook(() => useSandboxStatus(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isSuccess).toBe(true);
|
||||
});
|
||||
|
||||
expect(result.current.data?.sandbox_mode).toBe(false);
|
||||
});
|
||||
|
||||
it('handles sandbox not being enabled for business', async () => {
|
||||
const mockStatus = {
|
||||
sandbox_mode: false,
|
||||
sandbox_enabled: false,
|
||||
sandbox_schema: null,
|
||||
};
|
||||
vi.mocked(sandboxApi.getSandboxStatus).mockResolvedValue(mockStatus);
|
||||
|
||||
const { result } = renderHook(() => useSandboxStatus(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isSuccess).toBe(true);
|
||||
});
|
||||
|
||||
expect(result.current.data?.sandbox_enabled).toBe(false);
|
||||
expect(result.current.data?.sandbox_schema).toBeNull();
|
||||
});
|
||||
|
||||
it('handles API errors', async () => {
|
||||
vi.mocked(sandboxApi.getSandboxStatus).mockRejectedValue(
|
||||
new Error('Failed to fetch sandbox status')
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useSandboxStatus(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isError).toBe(true);
|
||||
});
|
||||
|
||||
expect(result.current.error).toBeInstanceOf(Error);
|
||||
expect(result.current.error?.message).toBe('Failed to fetch sandbox status');
|
||||
});
|
||||
|
||||
it('configures staleTime to 30 seconds', async () => {
|
||||
const mockStatus = {
|
||||
sandbox_mode: false,
|
||||
sandbox_enabled: true,
|
||||
sandbox_schema: 'business_1_sandbox',
|
||||
};
|
||||
vi.mocked(sandboxApi.getSandboxStatus).mockResolvedValue(mockStatus);
|
||||
|
||||
const { result } = renderHook(() => useSandboxStatus(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isSuccess).toBe(true);
|
||||
});
|
||||
|
||||
// Data should be considered fresh for 30 seconds
|
||||
// This is configured in the hook with staleTime: 30 * 1000
|
||||
expect(result.current.isStale).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useToggleSandbox', () => {
|
||||
let originalLocation: Location;
|
||||
let reloadMock: ReturnType<typeof vi.fn>;
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock window.location.reload
|
||||
originalLocation = window.location;
|
||||
reloadMock = vi.fn();
|
||||
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { ...originalLocation, reload: reloadMock },
|
||||
writable: true,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Restore window.location
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: originalLocation,
|
||||
writable: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('toggles sandbox mode to enabled', async () => {
|
||||
const mockResponse = {
|
||||
sandbox_mode: true,
|
||||
message: 'Sandbox mode enabled',
|
||||
};
|
||||
vi.mocked(sandboxApi.toggleSandboxMode).mockResolvedValue(mockResponse);
|
||||
|
||||
const { result } = renderHook(() => useToggleSandbox(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.mutateAsync(true);
|
||||
});
|
||||
|
||||
expect(sandboxApi.toggleSandboxMode).toHaveBeenCalled();
|
||||
expect(vi.mocked(sandboxApi.toggleSandboxMode).mock.calls[0][0]).toBe(true);
|
||||
});
|
||||
|
||||
it('toggles sandbox mode to disabled', async () => {
|
||||
const mockResponse = {
|
||||
sandbox_mode: false,
|
||||
message: 'Sandbox mode disabled',
|
||||
};
|
||||
vi.mocked(sandboxApi.toggleSandboxMode).mockResolvedValue(mockResponse);
|
||||
|
||||
const { result } = renderHook(() => useToggleSandbox(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.mutateAsync(false);
|
||||
});
|
||||
|
||||
expect(sandboxApi.toggleSandboxMode).toHaveBeenCalled();
|
||||
expect(vi.mocked(sandboxApi.toggleSandboxMode).mock.calls[0][0]).toBe(false);
|
||||
});
|
||||
|
||||
it('updates sandbox status in cache on success', async () => {
|
||||
const mockResponse = {
|
||||
sandbox_mode: true,
|
||||
message: 'Sandbox mode enabled',
|
||||
};
|
||||
vi.mocked(sandboxApi.toggleSandboxMode).mockResolvedValue(mockResponse);
|
||||
|
||||
const wrapper = createWrapper();
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
});
|
||||
|
||||
// Pre-populate cache with initial status
|
||||
queryClient.setQueryData(['sandboxStatus'], {
|
||||
sandbox_mode: false,
|
||||
sandbox_enabled: true,
|
||||
sandbox_schema: 'business_1_sandbox',
|
||||
});
|
||||
|
||||
const CustomWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
return React.createElement(QueryClientProvider, { client: queryClient }, children);
|
||||
};
|
||||
|
||||
const { result } = renderHook(() => useToggleSandbox(), {
|
||||
wrapper: CustomWrapper,
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.mutateAsync(true);
|
||||
});
|
||||
|
||||
// Verify cache was updated
|
||||
const cachedData = queryClient.getQueryData(['sandboxStatus']);
|
||||
expect(cachedData).toEqual({
|
||||
sandbox_mode: true,
|
||||
sandbox_enabled: true,
|
||||
sandbox_schema: 'business_1_sandbox',
|
||||
});
|
||||
});
|
||||
|
||||
it('reloads window after successful toggle', async () => {
|
||||
const mockResponse = {
|
||||
sandbox_mode: true,
|
||||
message: 'Sandbox mode enabled',
|
||||
};
|
||||
vi.mocked(sandboxApi.toggleSandboxMode).mockResolvedValue(mockResponse);
|
||||
|
||||
const { result } = renderHook(() => useToggleSandbox(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.mutateAsync(true);
|
||||
});
|
||||
|
||||
expect(reloadMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('handles toggle errors without reloading', async () => {
|
||||
vi.mocked(sandboxApi.toggleSandboxMode).mockRejectedValue(
|
||||
new Error('Failed to toggle sandbox mode')
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useToggleSandbox(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
try {
|
||||
await result.current.mutateAsync(true);
|
||||
} catch (error) {
|
||||
// Expected to throw
|
||||
}
|
||||
});
|
||||
|
||||
expect(reloadMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('updates cache even when old data is undefined', async () => {
|
||||
const mockResponse = {
|
||||
sandbox_mode: true,
|
||||
message: 'Sandbox mode enabled',
|
||||
};
|
||||
vi.mocked(sandboxApi.toggleSandboxMode).mockResolvedValue(mockResponse);
|
||||
|
||||
const wrapper = createWrapper();
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
});
|
||||
|
||||
const CustomWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
return React.createElement(QueryClientProvider, { client: queryClient }, children);
|
||||
};
|
||||
|
||||
const { result } = renderHook(() => useToggleSandbox(), {
|
||||
wrapper: CustomWrapper,
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.mutateAsync(true);
|
||||
});
|
||||
|
||||
// Verify cache was set even with no prior data
|
||||
const cachedData = queryClient.getQueryData(['sandboxStatus']);
|
||||
expect(cachedData).toMatchObject({
|
||||
sandbox_mode: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('useResetSandbox', () => {
|
||||
it('resets sandbox data', async () => {
|
||||
const mockResponse = {
|
||||
message: 'Sandbox data reset successfully',
|
||||
sandbox_schema: 'business_1_sandbox',
|
||||
};
|
||||
vi.mocked(sandboxApi.resetSandboxData).mockResolvedValue(mockResponse);
|
||||
|
||||
const { result } = renderHook(() => useResetSandbox(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.mutateAsync();
|
||||
});
|
||||
|
||||
expect(sandboxApi.resetSandboxData).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('invalidates resource queries on success', async () => {
|
||||
const mockResponse = {
|
||||
message: 'Sandbox data reset successfully',
|
||||
sandbox_schema: 'business_1_sandbox',
|
||||
};
|
||||
vi.mocked(sandboxApi.resetSandboxData).mockResolvedValue(mockResponse);
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
});
|
||||
|
||||
const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries');
|
||||
|
||||
const CustomWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
return React.createElement(QueryClientProvider, { client: queryClient }, children);
|
||||
};
|
||||
|
||||
const { result } = renderHook(() => useResetSandbox(), {
|
||||
wrapper: CustomWrapper,
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.mutateAsync();
|
||||
});
|
||||
|
||||
expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['resources'] });
|
||||
});
|
||||
|
||||
it('invalidates event queries on success', async () => {
|
||||
const mockResponse = {
|
||||
message: 'Sandbox data reset successfully',
|
||||
sandbox_schema: 'business_1_sandbox',
|
||||
};
|
||||
vi.mocked(sandboxApi.resetSandboxData).mockResolvedValue(mockResponse);
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
});
|
||||
|
||||
const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries');
|
||||
|
||||
const CustomWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
return React.createElement(QueryClientProvider, { client: queryClient }, children);
|
||||
};
|
||||
|
||||
const { result } = renderHook(() => useResetSandbox(), {
|
||||
wrapper: CustomWrapper,
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.mutateAsync();
|
||||
});
|
||||
|
||||
expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['events'] });
|
||||
});
|
||||
|
||||
it('invalidates service queries on success', async () => {
|
||||
const mockResponse = {
|
||||
message: 'Sandbox data reset successfully',
|
||||
sandbox_schema: 'business_1_sandbox',
|
||||
};
|
||||
vi.mocked(sandboxApi.resetSandboxData).mockResolvedValue(mockResponse);
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
});
|
||||
|
||||
const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries');
|
||||
|
||||
const CustomWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
return React.createElement(QueryClientProvider, { client: queryClient }, children);
|
||||
};
|
||||
|
||||
const { result } = renderHook(() => useResetSandbox(), {
|
||||
wrapper: CustomWrapper,
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.mutateAsync();
|
||||
});
|
||||
|
||||
expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['services'] });
|
||||
});
|
||||
|
||||
it('invalidates customer queries on success', async () => {
|
||||
const mockResponse = {
|
||||
message: 'Sandbox data reset successfully',
|
||||
sandbox_schema: 'business_1_sandbox',
|
||||
};
|
||||
vi.mocked(sandboxApi.resetSandboxData).mockResolvedValue(mockResponse);
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
});
|
||||
|
||||
const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries');
|
||||
|
||||
const CustomWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
return React.createElement(QueryClientProvider, { client: queryClient }, children);
|
||||
};
|
||||
|
||||
const { result } = renderHook(() => useResetSandbox(), {
|
||||
wrapper: CustomWrapper,
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.mutateAsync();
|
||||
});
|
||||
|
||||
expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['customers'] });
|
||||
});
|
||||
|
||||
it('invalidates payment queries on success', async () => {
|
||||
const mockResponse = {
|
||||
message: 'Sandbox data reset successfully',
|
||||
sandbox_schema: 'business_1_sandbox',
|
||||
};
|
||||
vi.mocked(sandboxApi.resetSandboxData).mockResolvedValue(mockResponse);
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
});
|
||||
|
||||
const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries');
|
||||
|
||||
const CustomWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
return React.createElement(QueryClientProvider, { client: queryClient }, children);
|
||||
};
|
||||
|
||||
const { result } = renderHook(() => useResetSandbox(), {
|
||||
wrapper: CustomWrapper,
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.mutateAsync();
|
||||
});
|
||||
|
||||
expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['payments'] });
|
||||
});
|
||||
|
||||
it('invalidates all required queries on success', async () => {
|
||||
const mockResponse = {
|
||||
message: 'Sandbox data reset successfully',
|
||||
sandbox_schema: 'business_1_sandbox',
|
||||
};
|
||||
vi.mocked(sandboxApi.resetSandboxData).mockResolvedValue(mockResponse);
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
});
|
||||
|
||||
const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries');
|
||||
|
||||
const CustomWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
return React.createElement(QueryClientProvider, { client: queryClient }, children);
|
||||
};
|
||||
|
||||
const { result } = renderHook(() => useResetSandbox(), {
|
||||
wrapper: CustomWrapper,
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.mutateAsync();
|
||||
});
|
||||
|
||||
// Verify all expected queries were invalidated
|
||||
expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['resources'] });
|
||||
expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['events'] });
|
||||
expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['services'] });
|
||||
expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['customers'] });
|
||||
expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['payments'] });
|
||||
expect(invalidateSpy).toHaveBeenCalledTimes(5);
|
||||
});
|
||||
|
||||
it('handles reset errors', async () => {
|
||||
vi.mocked(sandboxApi.resetSandboxData).mockRejectedValue(
|
||||
new Error('Failed to reset sandbox data')
|
||||
);
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
});
|
||||
|
||||
const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries');
|
||||
|
||||
const CustomWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
return React.createElement(QueryClientProvider, { client: queryClient }, children);
|
||||
};
|
||||
|
||||
const { result } = renderHook(() => useResetSandbox(), {
|
||||
wrapper: CustomWrapper,
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
try {
|
||||
await result.current.mutateAsync();
|
||||
} catch (error) {
|
||||
// Expected to throw
|
||||
}
|
||||
});
|
||||
|
||||
// Verify queries were NOT invalidated on error
|
||||
expect(invalidateSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns response data on success', async () => {
|
||||
const mockResponse = {
|
||||
message: 'Sandbox data reset successfully',
|
||||
sandbox_schema: 'business_1_sandbox',
|
||||
};
|
||||
vi.mocked(sandboxApi.resetSandboxData).mockResolvedValue(mockResponse);
|
||||
|
||||
const { result } = renderHook(() => useResetSandbox(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
let response;
|
||||
await act(async () => {
|
||||
response = await result.current.mutateAsync();
|
||||
});
|
||||
|
||||
expect(response).toEqual(mockResponse);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user