- 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>
730 lines
20 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
});
|