Files
smoothschedule/frontend/src/hooks/__tests__/useBusinessOAuth.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

730 lines
20 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', () => ({
getBusinessOAuthSettings: vi.fn(),
updateBusinessOAuthSettings: vi.fn(),
}));
import {
useBusinessOAuthSettings,
useUpdateBusinessOAuthSettings,
} from '../useBusinessOAuth';
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('useBusinessOAuth hooks', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('useBusinessOAuthSettings', () => {
it('fetches business OAuth settings successfully', async () => {
const mockResponse = {
settings: {
enabledProviders: ['google', 'microsoft'],
allowRegistration: true,
autoLinkByEmail: false,
useCustomCredentials: false,
},
availableProviders: [
{
id: 'google',
name: 'Google',
icon: 'google-icon',
description: 'Sign in with Google',
},
{
id: 'microsoft',
name: 'Microsoft',
icon: 'microsoft-icon',
description: 'Sign in with Microsoft',
},
],
};
vi.mocked(businessApi.getBusinessOAuthSettings).mockResolvedValue(mockResponse);
const { result } = renderHook(() => useBusinessOAuthSettings(), {
wrapper: createWrapper(),
});
// Initially loading
expect(result.current.isLoading).toBe(true);
// Wait for success
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(businessApi.getBusinessOAuthSettings).toHaveBeenCalledTimes(1);
expect(result.current.data).toEqual(mockResponse);
expect(result.current.data?.settings.enabledProviders).toHaveLength(2);
expect(result.current.data?.availableProviders).toHaveLength(2);
});
it('handles empty enabled providers', async () => {
const mockResponse = {
settings: {
enabledProviders: [],
allowRegistration: false,
autoLinkByEmail: false,
useCustomCredentials: false,
},
availableProviders: [
{
id: 'google',
name: 'Google',
icon: 'google-icon',
description: 'Sign in with Google',
},
],
};
vi.mocked(businessApi.getBusinessOAuthSettings).mockResolvedValue(mockResponse);
const { result } = renderHook(() => useBusinessOAuthSettings(), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(result.current.data?.settings.enabledProviders).toEqual([]);
expect(result.current.data?.availableProviders).toHaveLength(1);
});
it('handles custom credentials enabled', async () => {
const mockResponse = {
settings: {
enabledProviders: ['google'],
allowRegistration: true,
autoLinkByEmail: true,
useCustomCredentials: true,
},
availableProviders: [
{
id: 'google',
name: 'Google',
icon: 'google-icon',
description: 'Sign in with Google',
},
],
};
vi.mocked(businessApi.getBusinessOAuthSettings).mockResolvedValue(mockResponse);
const { result } = renderHook(() => useBusinessOAuthSettings(), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(result.current.data?.settings.useCustomCredentials).toBe(true);
expect(result.current.data?.settings.autoLinkByEmail).toBe(true);
});
it('handles API error gracefully', async () => {
const mockError = new Error('Failed to fetch OAuth settings');
vi.mocked(businessApi.getBusinessOAuthSettings).mockRejectedValue(mockError);
const { result } = renderHook(() => useBusinessOAuthSettings(), {
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', async () => {
vi.mocked(businessApi.getBusinessOAuthSettings).mockRejectedValue(
new Error('404 Not Found')
);
const { result } = renderHook(() => useBusinessOAuthSettings(), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isError).toBe(true);
});
// Should be called only once (no retries)
expect(businessApi.getBusinessOAuthSettings).toHaveBeenCalledTimes(1);
});
it('caches data with 5 minute stale time', async () => {
const mockResponse = {
settings: {
enabledProviders: ['google'],
allowRegistration: true,
autoLinkByEmail: false,
useCustomCredentials: false,
},
availableProviders: [
{
id: 'google',
name: 'Google',
icon: 'google-icon',
description: 'Sign in with Google',
},
],
};
vi.mocked(businessApi.getBusinessOAuthSettings).mockResolvedValue(mockResponse);
const { result, rerender } = renderHook(() => useBusinessOAuthSettings(), {
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.getBusinessOAuthSettings).toHaveBeenCalledTimes(1);
});
});
describe('useUpdateBusinessOAuthSettings', () => {
it('updates enabled providers successfully', async () => {
const mockResponse = {
settings: {
enabledProviders: ['google', 'microsoft', 'github'],
allowRegistration: true,
autoLinkByEmail: false,
useCustomCredentials: false,
},
availableProviders: [
{
id: 'google',
name: 'Google',
icon: 'google-icon',
description: 'Sign in with Google',
},
{
id: 'microsoft',
name: 'Microsoft',
icon: 'microsoft-icon',
description: 'Sign in with Microsoft',
},
{
id: 'github',
name: 'GitHub',
icon: 'github-icon',
description: 'Sign in with GitHub',
},
],
};
vi.mocked(businessApi.updateBusinessOAuthSettings).mockResolvedValue(mockResponse);
const { result } = renderHook(() => useUpdateBusinessOAuthSettings(), {
wrapper: createWrapper(),
});
await act(async () => {
await result.current.mutateAsync({
enabledProviders: ['google', 'microsoft', 'github'],
});
});
expect(businessApi.updateBusinessOAuthSettings).toHaveBeenCalledWith({
enabledProviders: ['google', 'microsoft', 'github'],
});
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
});
it('updates allowRegistration flag', async () => {
const mockResponse = {
settings: {
enabledProviders: ['google'],
allowRegistration: false,
autoLinkByEmail: false,
useCustomCredentials: false,
},
availableProviders: [
{
id: 'google',
name: 'Google',
icon: 'google-icon',
description: 'Sign in with Google',
},
],
};
vi.mocked(businessApi.updateBusinessOAuthSettings).mockResolvedValue(mockResponse);
const { result } = renderHook(() => useUpdateBusinessOAuthSettings(), {
wrapper: createWrapper(),
});
await act(async () => {
await result.current.mutateAsync({
allowRegistration: false,
});
});
expect(businessApi.updateBusinessOAuthSettings).toHaveBeenCalledWith({
allowRegistration: false,
});
});
it('updates autoLinkByEmail flag', async () => {
const mockResponse = {
settings: {
enabledProviders: ['google'],
allowRegistration: true,
autoLinkByEmail: true,
useCustomCredentials: false,
},
availableProviders: [
{
id: 'google',
name: 'Google',
icon: 'google-icon',
description: 'Sign in with Google',
},
],
};
vi.mocked(businessApi.updateBusinessOAuthSettings).mockResolvedValue(mockResponse);
const { result } = renderHook(() => useUpdateBusinessOAuthSettings(), {
wrapper: createWrapper(),
});
await act(async () => {
await result.current.mutateAsync({
autoLinkByEmail: true,
});
});
expect(businessApi.updateBusinessOAuthSettings).toHaveBeenCalledWith({
autoLinkByEmail: true,
});
});
it('updates useCustomCredentials flag', async () => {
const mockResponse = {
settings: {
enabledProviders: ['google'],
allowRegistration: true,
autoLinkByEmail: false,
useCustomCredentials: true,
},
availableProviders: [
{
id: 'google',
name: 'Google',
icon: 'google-icon',
description: 'Sign in with Google',
},
],
};
vi.mocked(businessApi.updateBusinessOAuthSettings).mockResolvedValue(mockResponse);
const { result } = renderHook(() => useUpdateBusinessOAuthSettings(), {
wrapper: createWrapper(),
});
await act(async () => {
await result.current.mutateAsync({
useCustomCredentials: true,
});
});
expect(businessApi.updateBusinessOAuthSettings).toHaveBeenCalledWith({
useCustomCredentials: true,
});
});
it('updates multiple settings at once', async () => {
const mockResponse = {
settings: {
enabledProviders: ['google', 'microsoft'],
allowRegistration: false,
autoLinkByEmail: true,
useCustomCredentials: true,
},
availableProviders: [
{
id: 'google',
name: 'Google',
icon: 'google-icon',
description: 'Sign in with Google',
},
{
id: 'microsoft',
name: 'Microsoft',
icon: 'microsoft-icon',
description: 'Sign in with Microsoft',
},
],
};
vi.mocked(businessApi.updateBusinessOAuthSettings).mockResolvedValue(mockResponse);
const { result } = renderHook(() => useUpdateBusinessOAuthSettings(), {
wrapper: createWrapper(),
});
await act(async () => {
await result.current.mutateAsync({
enabledProviders: ['google', 'microsoft'],
allowRegistration: false,
autoLinkByEmail: true,
useCustomCredentials: true,
});
});
expect(businessApi.updateBusinessOAuthSettings).toHaveBeenCalledWith({
enabledProviders: ['google', 'microsoft'],
allowRegistration: false,
autoLinkByEmail: true,
useCustomCredentials: true,
});
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(result.current.data?.settings.enabledProviders).toHaveLength(2);
expect(result.current.data?.settings.allowRegistration).toBe(false);
expect(result.current.data?.settings.autoLinkByEmail).toBe(true);
expect(result.current.data?.settings.useCustomCredentials).toBe(true);
});
it('updates query cache on success', async () => {
const mockResponse = {
settings: {
enabledProviders: ['google'],
allowRegistration: true,
autoLinkByEmail: false,
useCustomCredentials: false,
},
availableProviders: [
{
id: 'google',
name: 'Google',
icon: 'google-icon',
description: 'Sign in with Google',
},
],
};
vi.mocked(businessApi.updateBusinessOAuthSettings).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(() => useUpdateBusinessOAuthSettings(), {
wrapper,
});
await act(async () => {
await result.current.mutateAsync({
enabledProviders: ['google'],
});
});
// Verify cache was updated
const cachedData = queryClient.getQueryData(['businessOAuthSettings']);
expect(cachedData).toEqual(mockResponse);
});
it('handles update error gracefully', async () => {
const mockError = new Error('Failed to update settings');
vi.mocked(businessApi.updateBusinessOAuthSettings).mockRejectedValue(mockError);
const { result } = renderHook(() => useUpdateBusinessOAuthSettings(), {
wrapper: createWrapper(),
});
let caughtError: any = null;
await act(async () => {
try {
await result.current.mutateAsync({
allowRegistration: true,
});
} catch (error) {
caughtError = error;
}
});
expect(caughtError).toEqual(mockError);
await waitFor(() => {
expect(result.current.isError).toBe(true);
});
expect(result.current.error).toEqual(mockError);
});
it('handles partial update with only enabledProviders', async () => {
const mockResponse = {
settings: {
enabledProviders: ['github'],
allowRegistration: true,
autoLinkByEmail: false,
useCustomCredentials: false,
},
availableProviders: [
{
id: 'github',
name: 'GitHub',
icon: 'github-icon',
description: 'Sign in with GitHub',
},
],
};
vi.mocked(businessApi.updateBusinessOAuthSettings).mockResolvedValue(mockResponse);
const { result } = renderHook(() => useUpdateBusinessOAuthSettings(), {
wrapper: createWrapper(),
});
await act(async () => {
await result.current.mutateAsync({
enabledProviders: ['github'],
});
});
expect(businessApi.updateBusinessOAuthSettings).toHaveBeenCalledWith({
enabledProviders: ['github'],
});
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(result.current.data?.settings.enabledProviders).toEqual(['github']);
});
it('handles empty enabled providers array', async () => {
const mockResponse = {
settings: {
enabledProviders: [],
allowRegistration: true,
autoLinkByEmail: false,
useCustomCredentials: false,
},
availableProviders: [
{
id: 'google',
name: 'Google',
icon: 'google-icon',
description: 'Sign in with Google',
},
],
};
vi.mocked(businessApi.updateBusinessOAuthSettings).mockResolvedValue(mockResponse);
const { result } = renderHook(() => useUpdateBusinessOAuthSettings(), {
wrapper: createWrapper(),
});
await act(async () => {
await result.current.mutateAsync({
enabledProviders: [],
});
});
expect(businessApi.updateBusinessOAuthSettings).toHaveBeenCalledWith({
enabledProviders: [],
});
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(result.current.data?.settings.enabledProviders).toEqual([]);
});
it('preserves availableProviders from backend response', async () => {
const mockResponse = {
settings: {
enabledProviders: ['google'],
allowRegistration: true,
autoLinkByEmail: false,
useCustomCredentials: false,
},
availableProviders: [
{
id: 'google',
name: 'Google',
icon: 'google-icon',
description: 'Sign in with Google',
},
{
id: 'microsoft',
name: 'Microsoft',
icon: 'microsoft-icon',
description: 'Sign in with Microsoft',
},
{
id: 'github',
name: 'GitHub',
icon: 'github-icon',
description: 'Sign in with GitHub',
},
],
};
vi.mocked(businessApi.updateBusinessOAuthSettings).mockResolvedValue(mockResponse);
const { result } = renderHook(() => useUpdateBusinessOAuthSettings(), {
wrapper: createWrapper(),
});
await act(async () => {
await result.current.mutateAsync({
enabledProviders: ['google'],
});
});
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(result.current.data?.availableProviders).toHaveLength(3);
expect(result.current.data?.availableProviders.map(p => p.id)).toEqual([
'google',
'microsoft',
'github',
]);
});
});
describe('integration tests', () => {
it('fetches settings then updates them', async () => {
const initialResponse = {
settings: {
enabledProviders: ['google'],
allowRegistration: true,
autoLinkByEmail: false,
useCustomCredentials: false,
},
availableProviders: [
{
id: 'google',
name: 'Google',
icon: 'google-icon',
description: 'Sign in with Google',
},
{
id: 'microsoft',
name: 'Microsoft',
icon: 'microsoft-icon',
description: 'Sign in with Microsoft',
},
],
};
const updatedResponse = {
settings: {
enabledProviders: ['google', 'microsoft'],
allowRegistration: true,
autoLinkByEmail: true,
useCustomCredentials: false,
},
availableProviders: [
{
id: 'google',
name: 'Google',
icon: 'google-icon',
description: 'Sign in with Google',
},
{
id: 'microsoft',
name: 'Microsoft',
icon: 'microsoft-icon',
description: 'Sign in with Microsoft',
},
],
};
vi.mocked(businessApi.getBusinessOAuthSettings).mockResolvedValue(initialResponse);
vi.mocked(businessApi.updateBusinessOAuthSettings).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 settings
const { result: fetchResult } = renderHook(() => useBusinessOAuthSettings(), {
wrapper,
});
await waitFor(() => {
expect(fetchResult.current.isSuccess).toBe(true);
});
expect(fetchResult.current.data?.settings.enabledProviders).toEqual(['google']);
expect(fetchResult.current.data?.settings.autoLinkByEmail).toBe(false);
// Update settings
const { result: updateResult } = renderHook(() => useUpdateBusinessOAuthSettings(), {
wrapper,
});
await act(async () => {
await updateResult.current.mutateAsync({
enabledProviders: ['google', 'microsoft'],
autoLinkByEmail: true,
});
});
// Verify cache was updated
const cachedData = queryClient.getQueryData(['businessOAuthSettings']);
expect(cachedData).toEqual(updatedResponse);
});
});
});