Files
smoothschedule/frontend/src/hooks/__tests__/useBusinessOAuthCredentials.test.ts
poduck 8dc2248f1f 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>
2025-12-08 02:36:46 -05:00

922 lines
26 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest';
import { renderHook, waitFor, act } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import React from 'react';
// Mock the business API
vi.mock('../../api/business', () => ({
getBusinessOAuthCredentials: vi.fn(),
updateBusinessOAuthCredentials: vi.fn(),
}));
import {
useBusinessOAuthCredentials,
useUpdateBusinessOAuthCredentials,
} from '../useBusinessOAuthCredentials';
import * as businessApi from '../../api/business';
// Create wrapper for React Query
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('useBusinessOAuthCredentials hooks', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('useBusinessOAuthCredentials', () => {
it('fetches business OAuth credentials successfully', async () => {
const mockResponse = {
credentials: {
google: {
client_id: 'google-client-id-123',
client_secret: 'google-client-secret-456',
has_secret: true,
},
microsoft: {
client_id: 'microsoft-client-id-789',
client_secret: 'microsoft-client-secret-012',
has_secret: true,
},
},
useCustomCredentials: true,
};
vi.mocked(businessApi.getBusinessOAuthCredentials).mockResolvedValue(mockResponse);
const { result } = renderHook(() => useBusinessOAuthCredentials(), {
wrapper: createWrapper(),
});
// Initially loading
expect(result.current.isLoading).toBe(true);
// Wait for success
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(businessApi.getBusinessOAuthCredentials).toHaveBeenCalledTimes(1);
expect(result.current.data).toEqual(mockResponse);
expect(result.current.data?.credentials.google.client_id).toBe('google-client-id-123');
expect(result.current.data?.credentials.google.has_secret).toBe(true);
expect(result.current.data?.credentials.microsoft.client_id).toBe('microsoft-client-id-789');
expect(result.current.data?.useCustomCredentials).toBe(true);
});
it('handles empty credentials', async () => {
const mockResponse = {
credentials: {},
useCustomCredentials: false,
};
vi.mocked(businessApi.getBusinessOAuthCredentials).mockResolvedValue(mockResponse);
const { result } = renderHook(() => useBusinessOAuthCredentials(), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(result.current.data?.credentials).toEqual({});
expect(result.current.data?.useCustomCredentials).toBe(false);
});
it('handles credentials with has_secret false', async () => {
const mockResponse = {
credentials: {
google: {
client_id: 'google-client-id-123',
client_secret: '',
has_secret: false,
},
},
useCustomCredentials: true,
};
vi.mocked(businessApi.getBusinessOAuthCredentials).mockResolvedValue(mockResponse);
const { result } = renderHook(() => useBusinessOAuthCredentials(), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(result.current.data?.credentials.google.has_secret).toBe(false);
expect(result.current.data?.credentials.google.client_secret).toBe('');
});
it('handles multiple providers with mixed credential states', async () => {
const mockResponse = {
credentials: {
google: {
client_id: 'google-client-id',
client_secret: 'google-secret',
has_secret: true,
},
microsoft: {
client_id: 'microsoft-client-id',
client_secret: '',
has_secret: false,
},
github: {
client_id: '',
client_secret: '',
has_secret: false,
},
},
useCustomCredentials: true,
};
vi.mocked(businessApi.getBusinessOAuthCredentials).mockResolvedValue(mockResponse);
const { result } = renderHook(() => useBusinessOAuthCredentials(), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(Object.keys(result.current.data?.credentials || {})).toHaveLength(3);
expect(result.current.data?.credentials.google.has_secret).toBe(true);
expect(result.current.data?.credentials.microsoft.has_secret).toBe(false);
expect(result.current.data?.credentials.github.has_secret).toBe(false);
});
it('handles API error gracefully', async () => {
const mockError = new Error('Failed to fetch OAuth credentials');
vi.mocked(businessApi.getBusinessOAuthCredentials).mockRejectedValue(mockError);
const { result } = renderHook(() => useBusinessOAuthCredentials(), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isError).toBe(true);
});
expect(result.current.error).toEqual(mockError);
expect(result.current.data).toBeUndefined();
});
it('does not retry on failure (404)', async () => {
vi.mocked(businessApi.getBusinessOAuthCredentials).mockRejectedValue(
new Error('404 Not Found')
);
const { result } = renderHook(() => useBusinessOAuthCredentials(), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isError).toBe(true);
});
// Should be called only once (no retries)
expect(businessApi.getBusinessOAuthCredentials).toHaveBeenCalledTimes(1);
});
it('caches data with 5 minute stale time', async () => {
const mockResponse = {
credentials: {
google: {
client_id: 'google-client-id',
client_secret: 'google-secret',
has_secret: true,
},
},
useCustomCredentials: true,
};
vi.mocked(businessApi.getBusinessOAuthCredentials).mockResolvedValue(mockResponse);
const { result, rerender } = renderHook(() => useBusinessOAuthCredentials(), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
// Rerender should use cached data (within stale time)
rerender();
// Should still only be called once
expect(businessApi.getBusinessOAuthCredentials).toHaveBeenCalledTimes(1);
});
it('handles 401 unauthorized error', async () => {
const mockError = new Error('401 Unauthorized');
vi.mocked(businessApi.getBusinessOAuthCredentials).mockRejectedValue(mockError);
const { result } = renderHook(() => useBusinessOAuthCredentials(), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isError).toBe(true);
});
expect(result.current.error).toEqual(mockError);
});
it('handles network error', async () => {
const mockError = new Error('Network Error');
vi.mocked(businessApi.getBusinessOAuthCredentials).mockRejectedValue(mockError);
const { result } = renderHook(() => useBusinessOAuthCredentials(), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isError).toBe(true);
});
expect(result.current.error).toEqual(mockError);
});
});
describe('useUpdateBusinessOAuthCredentials', () => {
it('updates credentials for a single provider successfully', async () => {
const mockResponse = {
credentials: {
google: {
client_id: 'new-google-client-id',
client_secret: 'new-google-secret',
has_secret: true,
},
},
useCustomCredentials: true,
};
vi.mocked(businessApi.updateBusinessOAuthCredentials).mockResolvedValue(mockResponse);
const { result } = renderHook(() => useUpdateBusinessOAuthCredentials(), {
wrapper: createWrapper(),
});
await act(async () => {
await result.current.mutateAsync({
credentials: {
google: {
client_id: 'new-google-client-id',
client_secret: 'new-google-secret',
},
},
});
});
expect(businessApi.updateBusinessOAuthCredentials).toHaveBeenCalledWith({
credentials: {
google: {
client_id: 'new-google-client-id',
client_secret: 'new-google-secret',
},
},
});
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(result.current.data?.credentials.google.client_id).toBe('new-google-client-id');
expect(result.current.data?.credentials.google.has_secret).toBe(true);
});
it('updates credentials for multiple providers', async () => {
const mockResponse = {
credentials: {
google: {
client_id: 'google-id',
client_secret: 'google-secret',
has_secret: true,
},
microsoft: {
client_id: 'microsoft-id',
client_secret: 'microsoft-secret',
has_secret: true,
},
},
useCustomCredentials: true,
};
vi.mocked(businessApi.updateBusinessOAuthCredentials).mockResolvedValue(mockResponse);
const { result } = renderHook(() => useUpdateBusinessOAuthCredentials(), {
wrapper: createWrapper(),
});
await act(async () => {
await result.current.mutateAsync({
credentials: {
google: {
client_id: 'google-id',
client_secret: 'google-secret',
},
microsoft: {
client_id: 'microsoft-id',
client_secret: 'microsoft-secret',
},
},
});
});
expect(businessApi.updateBusinessOAuthCredentials).toHaveBeenCalledWith({
credentials: {
google: {
client_id: 'google-id',
client_secret: 'google-secret',
},
microsoft: {
client_id: 'microsoft-id',
client_secret: 'microsoft-secret',
},
},
});
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(Object.keys(result.current.data?.credentials || {})).toHaveLength(2);
});
it('updates only client_id without client_secret', async () => {
const mockResponse = {
credentials: {
google: {
client_id: 'updated-google-id',
client_secret: 'existing-secret',
has_secret: true,
},
},
useCustomCredentials: true,
};
vi.mocked(businessApi.updateBusinessOAuthCredentials).mockResolvedValue(mockResponse);
const { result } = renderHook(() => useUpdateBusinessOAuthCredentials(), {
wrapper: createWrapper(),
});
await act(async () => {
await result.current.mutateAsync({
credentials: {
google: {
client_id: 'updated-google-id',
},
},
});
});
expect(businessApi.updateBusinessOAuthCredentials).toHaveBeenCalledWith({
credentials: {
google: {
client_id: 'updated-google-id',
},
},
});
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
});
it('updates only client_secret without client_id', async () => {
const mockResponse = {
credentials: {
google: {
client_id: 'existing-google-id',
client_secret: 'new-secret',
has_secret: true,
},
},
useCustomCredentials: true,
};
vi.mocked(businessApi.updateBusinessOAuthCredentials).mockResolvedValue(mockResponse);
const { result } = renderHook(() => useUpdateBusinessOAuthCredentials(), {
wrapper: createWrapper(),
});
await act(async () => {
await result.current.mutateAsync({
credentials: {
google: {
client_secret: 'new-secret',
},
},
});
});
expect(businessApi.updateBusinessOAuthCredentials).toHaveBeenCalledWith({
credentials: {
google: {
client_secret: 'new-secret',
},
},
});
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
});
it('updates useCustomCredentials flag only', async () => {
const mockResponse = {
credentials: {
google: {
client_id: 'google-id',
client_secret: 'google-secret',
has_secret: true,
},
},
useCustomCredentials: false,
};
vi.mocked(businessApi.updateBusinessOAuthCredentials).mockResolvedValue(mockResponse);
const { result } = renderHook(() => useUpdateBusinessOAuthCredentials(), {
wrapper: createWrapper(),
});
await act(async () => {
await result.current.mutateAsync({
useCustomCredentials: false,
});
});
expect(businessApi.updateBusinessOAuthCredentials).toHaveBeenCalledWith({
useCustomCredentials: false,
});
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(result.current.data?.useCustomCredentials).toBe(false);
});
it('updates both credentials and useCustomCredentials flag', async () => {
const mockResponse = {
credentials: {
google: {
client_id: 'custom-google-id',
client_secret: 'custom-google-secret',
has_secret: true,
},
},
useCustomCredentials: true,
};
vi.mocked(businessApi.updateBusinessOAuthCredentials).mockResolvedValue(mockResponse);
const { result } = renderHook(() => useUpdateBusinessOAuthCredentials(), {
wrapper: createWrapper(),
});
await act(async () => {
await result.current.mutateAsync({
credentials: {
google: {
client_id: 'custom-google-id',
client_secret: 'custom-google-secret',
},
},
useCustomCredentials: true,
});
});
expect(businessApi.updateBusinessOAuthCredentials).toHaveBeenCalledWith({
credentials: {
google: {
client_id: 'custom-google-id',
client_secret: 'custom-google-secret',
},
},
useCustomCredentials: true,
});
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(result.current.data?.useCustomCredentials).toBe(true);
expect(result.current.data?.credentials.google.has_secret).toBe(true);
});
it('updates query cache on success', async () => {
const mockResponse = {
credentials: {
google: {
client_id: 'google-id',
client_secret: 'google-secret',
has_secret: true,
},
},
useCustomCredentials: true,
};
vi.mocked(businessApi.updateBusinessOAuthCredentials).mockResolvedValue(mockResponse);
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});
const wrapper = ({ children }: { children: React.ReactNode }) =>
React.createElement(QueryClientProvider, { client: queryClient }, children);
const { result } = renderHook(() => useUpdateBusinessOAuthCredentials(), {
wrapper,
});
await act(async () => {
await result.current.mutateAsync({
credentials: {
google: {
client_id: 'google-id',
client_secret: 'google-secret',
},
},
});
});
// Verify cache was updated
const cachedData = queryClient.getQueryData(['businessOAuthCredentials']);
expect(cachedData).toEqual(mockResponse);
});
it('handles update error gracefully', async () => {
const mockError = new Error('Failed to update credentials');
vi.mocked(businessApi.updateBusinessOAuthCredentials).mockRejectedValue(mockError);
const { result } = renderHook(() => useUpdateBusinessOAuthCredentials(), {
wrapper: createWrapper(),
});
let caughtError: any = null;
await act(async () => {
try {
await result.current.mutateAsync({
credentials: {
google: {
client_id: 'test-id',
},
},
});
} catch (error) {
caughtError = error;
}
});
expect(caughtError).toEqual(mockError);
await waitFor(() => {
expect(result.current.isError).toBe(true);
});
expect(result.current.error).toEqual(mockError);
});
it('handles validation error from API', async () => {
const mockError = new Error('Invalid client_id format');
vi.mocked(businessApi.updateBusinessOAuthCredentials).mockRejectedValue(mockError);
const { result } = renderHook(() => useUpdateBusinessOAuthCredentials(), {
wrapper: createWrapper(),
});
let caughtError: any = null;
await act(async () => {
try {
await result.current.mutateAsync({
credentials: {
google: {
client_id: 'invalid-format',
},
},
});
} catch (error) {
caughtError = error;
}
});
expect(caughtError).toEqual(mockError);
await waitFor(() => {
expect(result.current.isError).toBe(true);
});
});
it('handles clearing credentials by passing empty values', async () => {
const mockResponse = {
credentials: {
google: {
client_id: '',
client_secret: '',
has_secret: false,
},
},
useCustomCredentials: false,
};
vi.mocked(businessApi.updateBusinessOAuthCredentials).mockResolvedValue(mockResponse);
const { result } = renderHook(() => useUpdateBusinessOAuthCredentials(), {
wrapper: createWrapper(),
});
await act(async () => {
await result.current.mutateAsync({
credentials: {
google: {
client_id: '',
client_secret: '',
},
},
useCustomCredentials: false,
});
});
expect(businessApi.updateBusinessOAuthCredentials).toHaveBeenCalledWith({
credentials: {
google: {
client_id: '',
client_secret: '',
},
},
useCustomCredentials: false,
});
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(result.current.data?.credentials.google.has_secret).toBe(false);
expect(result.current.data?.useCustomCredentials).toBe(false);
});
it('handles permission error (403)', async () => {
const mockError = new Error('403 Forbidden - Insufficient permissions');
vi.mocked(businessApi.updateBusinessOAuthCredentials).mockRejectedValue(mockError);
const { result } = renderHook(() => useUpdateBusinessOAuthCredentials(), {
wrapper: createWrapper(),
});
let caughtError: any = null;
await act(async () => {
try {
await result.current.mutateAsync({
useCustomCredentials: true,
});
} catch (error) {
caughtError = error;
}
});
expect(caughtError).toEqual(mockError);
await waitFor(() => {
expect(result.current.isError).toBe(true);
});
});
it('preserves backend response structure with has_secret flags', async () => {
const mockResponse = {
credentials: {
google: {
client_id: 'google-id',
client_secret: 'google-secret',
has_secret: true,
},
microsoft: {
client_id: 'microsoft-id',
client_secret: '',
has_secret: false,
},
},
useCustomCredentials: true,
};
vi.mocked(businessApi.updateBusinessOAuthCredentials).mockResolvedValue(mockResponse);
const { result } = renderHook(() => useUpdateBusinessOAuthCredentials(), {
wrapper: createWrapper(),
});
await act(async () => {
await result.current.mutateAsync({
credentials: {
google: {
client_id: 'google-id',
client_secret: 'google-secret',
},
microsoft: {
client_id: 'microsoft-id',
},
},
});
});
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(result.current.data?.credentials.google.has_secret).toBe(true);
expect(result.current.data?.credentials.microsoft.has_secret).toBe(false);
expect(result.current.data?.credentials.microsoft.client_secret).toBe('');
});
});
describe('integration tests', () => {
it('fetches credentials then updates them', async () => {
const initialResponse = {
credentials: {
google: {
client_id: 'initial-google-id',
client_secret: 'initial-google-secret',
has_secret: true,
},
},
useCustomCredentials: true,
};
const updatedResponse = {
credentials: {
google: {
client_id: 'updated-google-id',
client_secret: 'updated-google-secret',
has_secret: true,
},
microsoft: {
client_id: 'new-microsoft-id',
client_secret: 'new-microsoft-secret',
has_secret: true,
},
},
useCustomCredentials: true,
};
vi.mocked(businessApi.getBusinessOAuthCredentials).mockResolvedValue(initialResponse);
vi.mocked(businessApi.updateBusinessOAuthCredentials).mockResolvedValue(updatedResponse);
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});
const wrapper = ({ children }: { children: React.ReactNode }) =>
React.createElement(QueryClientProvider, { client: queryClient }, children);
// Fetch initial credentials
const { result: fetchResult } = renderHook(() => useBusinessOAuthCredentials(), {
wrapper,
});
await waitFor(() => {
expect(fetchResult.current.isSuccess).toBe(true);
});
expect(fetchResult.current.data?.credentials.google.client_id).toBe('initial-google-id');
expect(Object.keys(fetchResult.current.data?.credentials || {})).toHaveLength(1);
// Update credentials
const { result: updateResult } = renderHook(() => useUpdateBusinessOAuthCredentials(), {
wrapper,
});
await act(async () => {
await updateResult.current.mutateAsync({
credentials: {
google: {
client_id: 'updated-google-id',
client_secret: 'updated-google-secret',
},
microsoft: {
client_id: 'new-microsoft-id',
client_secret: 'new-microsoft-secret',
},
},
});
});
// Verify cache was updated
const cachedData = queryClient.getQueryData(['businessOAuthCredentials']);
expect(cachedData).toEqual(updatedResponse);
expect((cachedData as any).credentials.google.client_id).toBe('updated-google-id');
expect((cachedData as any).credentials.microsoft.client_id).toBe('new-microsoft-id');
});
it('toggles custom credentials on and off', async () => {
const initialResponse = {
credentials: {
google: {
client_id: 'google-id',
client_secret: 'google-secret',
has_secret: true,
},
},
useCustomCredentials: true,
};
const toggledOffResponse = {
credentials: {
google: {
client_id: 'google-id',
client_secret: 'google-secret',
has_secret: true,
},
},
useCustomCredentials: false,
};
const toggledOnResponse = {
credentials: {
google: {
client_id: 'google-id',
client_secret: 'google-secret',
has_secret: true,
},
},
useCustomCredentials: true,
};
vi.mocked(businessApi.getBusinessOAuthCredentials).mockResolvedValue(initialResponse);
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});
const wrapper = ({ children }: { children: React.ReactNode }) =>
React.createElement(QueryClientProvider, { client: queryClient }, children);
// Fetch initial state
const { result: fetchResult } = renderHook(() => useBusinessOAuthCredentials(), {
wrapper,
});
await waitFor(() => {
expect(fetchResult.current.isSuccess).toBe(true);
});
expect(fetchResult.current.data?.useCustomCredentials).toBe(true);
// Toggle off
const { result: updateResult } = renderHook(() => useUpdateBusinessOAuthCredentials(), {
wrapper,
});
vi.mocked(businessApi.updateBusinessOAuthCredentials).mockResolvedValue(toggledOffResponse);
await act(async () => {
await updateResult.current.mutateAsync({
useCustomCredentials: false,
});
});
let cachedData = queryClient.getQueryData(['businessOAuthCredentials']);
expect((cachedData as any).useCustomCredentials).toBe(false);
// Toggle back on
vi.mocked(businessApi.updateBusinessOAuthCredentials).mockResolvedValue(toggledOnResponse);
await act(async () => {
await updateResult.current.mutateAsync({
useCustomCredentials: true,
});
});
cachedData = queryClient.getQueryData(['businessOAuthCredentials']);
expect((cachedData as any).useCustomCredentials).toBe(true);
});
});
});