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

1031 lines
31 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';
vi.mock('../../api/ticketEmailSettings', () => ({
getTicketEmailSettings: vi.fn(),
updateTicketEmailSettings: vi.fn(),
testImapConnection: vi.fn(),
testSmtpConnection: vi.fn(),
fetchEmailsNow: vi.fn(),
getIncomingEmails: vi.fn(),
reprocessIncomingEmail: vi.fn(),
detectEmailProvider: vi.fn(),
getOAuthStatus: vi.fn(),
initiateGoogleOAuth: vi.fn(),
initiateMicrosoftOAuth: vi.fn(),
getOAuthCredentials: vi.fn(),
deleteOAuthCredential: vi.fn(),
}));
import {
useTicketEmailSettings,
useUpdateTicketEmailSettings,
useTestImapConnection,
useTestSmtpConnection,
useFetchEmailsNow,
useIncomingEmails,
useReprocessIncomingEmail,
useDetectEmailProvider,
useOAuthStatus,
useInitiateGoogleOAuth,
useInitiateMicrosoftOAuth,
useOAuthCredentials,
useDeleteOAuthCredential,
} from '../useTicketEmailSettings';
import * as api from '../../api/ticketEmailSettings';
// 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('useTicketEmailSettings hooks', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('useTicketEmailSettings', () => {
it('fetches ticket email settings', async () => {
const mockSettings = {
imap_host: 'imap.gmail.com',
imap_port: 993,
imap_use_ssl: true,
imap_username: 'support@example.com',
imap_password_masked: '***',
imap_folder: 'INBOX',
smtp_host: 'smtp.gmail.com',
smtp_port: 587,
smtp_use_tls: true,
smtp_use_ssl: false,
smtp_username: 'support@example.com',
smtp_password_masked: '***',
smtp_from_email: 'support@example.com',
smtp_from_name: 'Support Team',
support_email_address: 'support@example.com',
support_email_domain: 'example.com',
is_enabled: true,
delete_after_processing: false,
check_interval_seconds: 300,
max_attachment_size_mb: 10,
allowed_attachment_types: ['pdf', 'png', 'jpg'],
last_check_at: '2025-01-01T12:00:00Z',
last_error: '',
emails_processed_count: 42,
is_configured: true,
is_imap_configured: true,
is_smtp_configured: true,
created_at: '2025-01-01T00:00:00Z',
updated_at: '2025-01-01T12:00:00Z',
};
vi.mocked(api.getTicketEmailSettings).mockResolvedValue(mockSettings);
const { result } = renderHook(() => useTicketEmailSettings(), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(api.getTicketEmailSettings).toHaveBeenCalledOnce();
expect(result.current.data).toEqual(mockSettings);
expect(result.current.data?.imap_host).toBe('imap.gmail.com');
expect(result.current.data?.smtp_host).toBe('smtp.gmail.com');
});
it('handles error when fetching settings fails', async () => {
vi.mocked(api.getTicketEmailSettings).mockRejectedValue(new Error('Network error'));
const { result } = renderHook(() => useTicketEmailSettings(), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isError).toBe(true);
});
expect(result.current.error).toBeDefined();
});
});
describe('useUpdateTicketEmailSettings', () => {
it('updates ticket email settings', async () => {
const mockUpdate = {
imap_host: 'imap.gmail.com',
imap_port: 993,
imap_use_ssl: true,
};
const mockResponse = {
...mockUpdate,
imap_username: 'support@example.com',
imap_password_masked: '***',
imap_folder: 'INBOX',
smtp_host: 'smtp.gmail.com',
smtp_port: 587,
smtp_use_tls: true,
smtp_use_ssl: false,
smtp_username: 'support@example.com',
smtp_password_masked: '***',
smtp_from_email: 'support@example.com',
smtp_from_name: 'Support',
support_email_address: 'support@example.com',
support_email_domain: 'example.com',
is_enabled: true,
delete_after_processing: false,
check_interval_seconds: 300,
max_attachment_size_mb: 10,
allowed_attachment_types: ['pdf'],
last_check_at: null,
last_error: '',
emails_processed_count: 0,
is_configured: true,
is_imap_configured: true,
is_smtp_configured: true,
created_at: '2025-01-01T00:00:00Z',
updated_at: '2025-01-01T12:00:00Z',
};
vi.mocked(api.updateTicketEmailSettings).mockResolvedValue(mockResponse);
const { result } = renderHook(() => useUpdateTicketEmailSettings(), {
wrapper: createWrapper(),
});
let mutationResult;
await act(async () => {
mutationResult = await result.current.mutateAsync(mockUpdate);
});
expect(api.updateTicketEmailSettings).toHaveBeenCalledWith(mockUpdate);
expect(mutationResult).toEqual(mockResponse);
});
it('invalidates query cache on successful update', async () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});
const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries');
const mockResponse = {
imap_host: 'imap.gmail.com',
imap_port: 993,
imap_use_ssl: true,
imap_username: 'support@example.com',
imap_password_masked: '***',
imap_folder: 'INBOX',
smtp_host: 'smtp.gmail.com',
smtp_port: 587,
smtp_use_tls: true,
smtp_use_ssl: false,
smtp_username: 'support@example.com',
smtp_password_masked: '***',
smtp_from_email: 'support@example.com',
smtp_from_name: 'Support',
support_email_address: 'support@example.com',
support_email_domain: 'example.com',
is_enabled: true,
delete_after_processing: false,
check_interval_seconds: 300,
max_attachment_size_mb: 10,
allowed_attachment_types: [],
last_check_at: null,
last_error: '',
emails_processed_count: 0,
is_configured: true,
is_imap_configured: true,
is_smtp_configured: true,
created_at: '2025-01-01T00:00:00Z',
updated_at: '2025-01-01T12:00:00Z',
};
vi.mocked(api.updateTicketEmailSettings).mockResolvedValue(mockResponse);
const wrapper = ({ children }: { children: React.ReactNode }) =>
React.createElement(QueryClientProvider, { client: queryClient }, children);
const { result } = renderHook(() => useUpdateTicketEmailSettings(), { wrapper });
await act(async () => {
await result.current.mutateAsync({ is_enabled: true });
});
expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['ticketEmailSettings'] });
});
it('handles validation errors', async () => {
vi.mocked(api.updateTicketEmailSettings).mockRejectedValue(
new Error('Invalid IMAP port')
);
const { result } = renderHook(() => useUpdateTicketEmailSettings(), {
wrapper: createWrapper(),
});
let error;
await act(async () => {
try {
await result.current.mutateAsync({ imap_port: -1 });
} catch (e) {
error = e;
}
});
expect(error).toBeDefined();
expect(error).toBeInstanceOf(Error);
});
});
describe('useTestImapConnection', () => {
it('successfully tests IMAP connection', async () => {
const mockResult = {
success: true,
message: 'IMAP connection successful',
};
vi.mocked(api.testImapConnection).mockResolvedValue(mockResult);
const { result } = renderHook(() => useTestImapConnection(), {
wrapper: createWrapper(),
});
let mutationResult;
await act(async () => {
mutationResult = await result.current.mutateAsync();
});
expect(api.testImapConnection).toHaveBeenCalledOnce();
expect(mutationResult).toEqual(mockResult);
expect(mutationResult?.success).toBe(true);
});
it('handles IMAP connection failure', async () => {
const mockResult = {
success: false,
message: 'Authentication failed',
};
vi.mocked(api.testImapConnection).mockResolvedValue(mockResult);
const { result } = renderHook(() => useTestImapConnection(), {
wrapper: createWrapper(),
});
let mutationResult;
await act(async () => {
mutationResult = await result.current.mutateAsync();
});
expect(mutationResult?.success).toBe(false);
expect(mutationResult?.message).toBe('Authentication failed');
});
});
describe('useTestSmtpConnection', () => {
it('successfully tests SMTP connection', async () => {
const mockResult = {
success: true,
message: 'SMTP connection successful',
};
vi.mocked(api.testSmtpConnection).mockResolvedValue(mockResult);
const { result } = renderHook(() => useTestSmtpConnection(), {
wrapper: createWrapper(),
});
let mutationResult;
await act(async () => {
mutationResult = await result.current.mutateAsync();
});
expect(api.testSmtpConnection).toHaveBeenCalledOnce();
expect(mutationResult).toEqual(mockResult);
expect(mutationResult?.success).toBe(true);
});
it('handles SMTP connection failure', async () => {
const mockResult = {
success: false,
message: 'Connection timeout',
};
vi.mocked(api.testSmtpConnection).mockResolvedValue(mockResult);
const { result } = renderHook(() => useTestSmtpConnection(), {
wrapper: createWrapper(),
});
let mutationResult;
await act(async () => {
mutationResult = await result.current.mutateAsync();
});
expect(mutationResult?.success).toBe(false);
expect(mutationResult?.message).toBe('Connection timeout');
});
});
describe('useFetchEmailsNow', () => {
it('manually fetches emails', async () => {
const mockResult = {
success: true,
message: 'Fetched 5 new emails',
processed: 5,
};
vi.mocked(api.fetchEmailsNow).mockResolvedValue(mockResult);
const { result } = renderHook(() => useFetchEmailsNow(), {
wrapper: createWrapper(),
});
let mutationResult;
await act(async () => {
mutationResult = await result.current.mutateAsync();
});
expect(api.fetchEmailsNow).toHaveBeenCalledOnce();
expect(mutationResult?.processed).toBe(5);
});
it('invalidates queries after fetching emails', async () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});
const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries');
const mockResult = {
success: true,
message: 'Fetched 3 emails',
processed: 3,
};
vi.mocked(api.fetchEmailsNow).mockResolvedValue(mockResult);
const wrapper = ({ children }: { children: React.ReactNode }) =>
React.createElement(QueryClientProvider, { client: queryClient }, children);
const { result } = renderHook(() => useFetchEmailsNow(), { wrapper });
await act(async () => {
await result.current.mutateAsync();
});
expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['ticketEmailSettings'] });
expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['incomingTicketEmails'] });
});
it('handles fetch error', async () => {
const mockResult = {
success: false,
message: 'IMAP connection failed',
processed: 0,
};
vi.mocked(api.fetchEmailsNow).mockResolvedValue(mockResult);
const { result } = renderHook(() => useFetchEmailsNow(), {
wrapper: createWrapper(),
});
let mutationResult;
await act(async () => {
mutationResult = await result.current.mutateAsync();
});
expect(mutationResult?.success).toBe(false);
expect(mutationResult?.processed).toBe(0);
});
});
describe('useIncomingEmails', () => {
it('fetches incoming emails without filters', async () => {
const mockEmails = [
{
id: 1,
message_id: '<msg-1@example.com>',
from_address: 'customer@example.com',
from_name: 'John Doe',
to_address: 'support@example.com',
subject: 'Help with order',
body_text: 'I need help with my order',
extracted_reply: 'I need help',
ticket: 101,
ticket_subject: 'Help with order',
matched_user: 42,
ticket_id_from_email: '101',
processing_status: 'PROCESSED' as const,
processing_status_display: 'Processed',
error_message: '',
email_date: '2025-01-01T10:00:00Z',
received_at: '2025-01-01T10:05:00Z',
processed_at: '2025-01-01T10:06:00Z',
},
];
vi.mocked(api.getIncomingEmails).mockResolvedValue(mockEmails);
const { result } = renderHook(() => useIncomingEmails(), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(api.getIncomingEmails).toHaveBeenCalledWith(undefined);
expect(result.current.data).toHaveLength(1);
expect(result.current.data?.[0].from_address).toBe('customer@example.com');
});
it('fetches incoming emails with status filter', async () => {
const mockEmails = [
{
id: 2,
message_id: '<msg-2@example.com>',
from_address: 'user@example.com',
from_name: 'Jane Smith',
to_address: 'support@example.com',
subject: 'Failed email',
body_text: 'Test',
extracted_reply: 'Test',
ticket: null,
ticket_subject: '',
matched_user: null,
ticket_id_from_email: '',
processing_status: 'FAILED' as const,
processing_status_display: 'Failed',
error_message: 'Could not parse ticket ID',
email_date: '2025-01-01T11:00:00Z',
received_at: '2025-01-01T11:05:00Z',
processed_at: null,
},
];
vi.mocked(api.getIncomingEmails).mockResolvedValue(mockEmails);
const { result } = renderHook(() => useIncomingEmails({ status: 'FAILED' }), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(api.getIncomingEmails).toHaveBeenCalledWith({ status: 'FAILED' });
expect(result.current.data?.[0].processing_status).toBe('FAILED');
});
it('fetches incoming emails with ticket filter', async () => {
vi.mocked(api.getIncomingEmails).mockResolvedValue([]);
const { result } = renderHook(() => useIncomingEmails({ ticket: 101 }), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(api.getIncomingEmails).toHaveBeenCalledWith({ ticket: 101 });
});
it('fetches incoming emails with multiple filters', async () => {
vi.mocked(api.getIncomingEmails).mockResolvedValue([]);
const { result } = renderHook(
() => useIncomingEmails({ status: 'PROCESSED', ticket: 101 }),
{ wrapper: createWrapper() }
);
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(api.getIncomingEmails).toHaveBeenCalledWith({
status: 'PROCESSED',
ticket: 101,
});
});
});
describe('useReprocessIncomingEmail', () => {
it('reprocesses failed incoming email', async () => {
const mockResult = {
success: true,
message: 'Email reprocessed successfully',
comment_id: 456,
ticket_id: 789,
};
vi.mocked(api.reprocessIncomingEmail).mockResolvedValue(mockResult);
const { result } = renderHook(() => useReprocessIncomingEmail(), {
wrapper: createWrapper(),
});
let mutationResult;
await act(async () => {
mutationResult = await result.current.mutateAsync(123);
});
expect(api.reprocessIncomingEmail).toHaveBeenCalledWith(123);
expect(mutationResult?.success).toBe(true);
expect(mutationResult?.ticket_id).toBe(789);
});
it('invalidates incoming emails cache on success', async () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});
const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries');
const mockResult = {
success: true,
message: 'Reprocessed',
};
vi.mocked(api.reprocessIncomingEmail).mockResolvedValue(mockResult);
const wrapper = ({ children }: { children: React.ReactNode }) =>
React.createElement(QueryClientProvider, { client: queryClient }, children);
const { result } = renderHook(() => useReprocessIncomingEmail(), { wrapper });
await act(async () => {
await result.current.mutateAsync(123);
});
expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['incomingTicketEmails'] });
});
it('handles reprocessing failure', async () => {
const mockResult = {
success: false,
message: 'Still cannot parse ticket ID',
};
vi.mocked(api.reprocessIncomingEmail).mockResolvedValue(mockResult);
const { result } = renderHook(() => useReprocessIncomingEmail(), {
wrapper: createWrapper(),
});
let mutationResult;
await act(async () => {
mutationResult = await result.current.mutateAsync(123);
});
expect(mutationResult?.success).toBe(false);
});
});
describe('useDetectEmailProvider', () => {
it('detects Gmail provider', async () => {
const mockResult = {
success: true,
email: 'user@gmail.com',
domain: 'gmail.com',
detected: true,
detected_via: 'domain_lookup' as const,
provider: 'google' as const,
display_name: 'Gmail',
imap_host: 'imap.gmail.com',
imap_port: 993,
smtp_host: 'smtp.gmail.com',
smtp_port: 587,
oauth_supported: true,
notes: 'OAuth recommended',
};
vi.mocked(api.detectEmailProvider).mockResolvedValue(mockResult);
const { result } = renderHook(() => useDetectEmailProvider(), {
wrapper: createWrapper(),
});
let mutationResult;
await act(async () => {
mutationResult = await result.current.mutateAsync('user@gmail.com');
});
expect(api.detectEmailProvider).toHaveBeenCalledWith('user@gmail.com');
expect(mutationResult?.provider).toBe('google');
expect(mutationResult?.oauth_supported).toBe(true);
});
it('detects Microsoft provider', async () => {
const mockResult = {
success: true,
email: 'user@outlook.com',
domain: 'outlook.com',
detected: true,
detected_via: 'domain_lookup' as const,
provider: 'microsoft' as const,
display_name: 'Outlook.com',
imap_host: 'outlook.office365.com',
imap_port: 993,
smtp_host: 'smtp.office365.com',
smtp_port: 587,
oauth_supported: true,
};
vi.mocked(api.detectEmailProvider).mockResolvedValue(mockResult);
const { result } = renderHook(() => useDetectEmailProvider(), {
wrapper: createWrapper(),
});
let mutationResult;
await act(async () => {
mutationResult = await result.current.mutateAsync('user@outlook.com');
});
expect(mutationResult?.provider).toBe('microsoft');
});
it('detects custom domain using MX records', async () => {
const mockResult = {
success: true,
email: 'user@company.com',
domain: 'company.com',
detected: true,
detected_via: 'mx_record' as const,
provider: 'google' as const,
display_name: 'Google Workspace',
oauth_supported: true,
message: 'Detected Google Workspace via MX records',
};
vi.mocked(api.detectEmailProvider).mockResolvedValue(mockResult);
const { result } = renderHook(() => useDetectEmailProvider(), {
wrapper: createWrapper(),
});
let mutationResult;
await act(async () => {
mutationResult = await result.current.mutateAsync('user@company.com');
});
expect(mutationResult?.detected_via).toBe('mx_record');
expect(mutationResult?.provider).toBe('google');
});
it('handles unknown provider', async () => {
const mockResult = {
success: true,
email: 'user@custom.com',
domain: 'custom.com',
detected: false,
provider: 'unknown' as const,
display_name: 'Unknown Provider',
oauth_supported: false,
message: 'Could not auto-detect provider',
};
vi.mocked(api.detectEmailProvider).mockResolvedValue(mockResult);
const { result } = renderHook(() => useDetectEmailProvider(), {
wrapper: createWrapper(),
});
let mutationResult;
await act(async () => {
mutationResult = await result.current.mutateAsync('user@custom.com');
});
expect(mutationResult?.detected).toBe(false);
expect(mutationResult?.provider).toBe('unknown');
});
});
describe('useOAuthStatus', () => {
it('fetches OAuth status for both providers', async () => {
const mockStatus = {
google: { configured: true },
microsoft: { configured: false },
};
vi.mocked(api.getOAuthStatus).mockResolvedValue(mockStatus);
const { result } = renderHook(() => useOAuthStatus(), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(api.getOAuthStatus).toHaveBeenCalledOnce();
expect(result.current.data?.google.configured).toBe(true);
expect(result.current.data?.microsoft.configured).toBe(false);
});
it('handles both providers not configured', async () => {
const mockStatus = {
google: { configured: false },
microsoft: { configured: false },
};
vi.mocked(api.getOAuthStatus).mockResolvedValue(mockStatus);
const { result } = renderHook(() => useOAuthStatus(), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(result.current.data?.google.configured).toBe(false);
expect(result.current.data?.microsoft.configured).toBe(false);
});
});
describe('useInitiateGoogleOAuth', () => {
it('initiates Google OAuth flow with default purpose', async () => {
const mockResult = {
success: true,
authorization_url: 'https://accounts.google.com/o/oauth2/auth?...',
};
vi.mocked(api.initiateGoogleOAuth).mockResolvedValue(mockResult);
const { result } = renderHook(() => useInitiateGoogleOAuth(), {
wrapper: createWrapper(),
});
let mutationResult;
await act(async () => {
mutationResult = await result.current.mutateAsync();
});
expect(api.initiateGoogleOAuth).toHaveBeenCalledWith('email');
expect(mutationResult?.success).toBe(true);
expect(mutationResult?.authorization_url).toContain('google.com');
});
it('initiates Google OAuth flow with custom purpose', async () => {
const mockResult = {
success: true,
authorization_url: 'https://accounts.google.com/o/oauth2/auth?...',
};
vi.mocked(api.initiateGoogleOAuth).mockResolvedValue(mockResult);
const { result } = renderHook(() => useInitiateGoogleOAuth(), {
wrapper: createWrapper(),
});
await act(async () => {
await result.current.mutateAsync('calendar');
});
expect(api.initiateGoogleOAuth).toHaveBeenCalledWith('calendar');
});
it('handles OAuth initiation error', async () => {
const mockResult = {
success: false,
error: 'OAuth not configured',
};
vi.mocked(api.initiateGoogleOAuth).mockResolvedValue(mockResult);
const { result } = renderHook(() => useInitiateGoogleOAuth(), {
wrapper: createWrapper(),
});
let mutationResult;
await act(async () => {
mutationResult = await result.current.mutateAsync();
});
expect(mutationResult?.success).toBe(false);
expect(mutationResult?.error).toBe('OAuth not configured');
});
});
describe('useInitiateMicrosoftOAuth', () => {
it('initiates Microsoft OAuth flow with default purpose', async () => {
const mockResult = {
success: true,
authorization_url: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize?...',
};
vi.mocked(api.initiateMicrosoftOAuth).mockResolvedValue(mockResult);
const { result } = renderHook(() => useInitiateMicrosoftOAuth(), {
wrapper: createWrapper(),
});
let mutationResult;
await act(async () => {
mutationResult = await result.current.mutateAsync();
});
expect(api.initiateMicrosoftOAuth).toHaveBeenCalledWith('email');
expect(mutationResult?.success).toBe(true);
expect(mutationResult?.authorization_url).toContain('microsoft');
});
it('initiates Microsoft OAuth flow with custom purpose', async () => {
const mockResult = {
success: true,
authorization_url: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize?...',
};
vi.mocked(api.initiateMicrosoftOAuth).mockResolvedValue(mockResult);
const { result } = renderHook(() => useInitiateMicrosoftOAuth(), {
wrapper: createWrapper(),
});
await act(async () => {
await result.current.mutateAsync('calendar');
});
expect(api.initiateMicrosoftOAuth).toHaveBeenCalledWith('calendar');
});
it('handles OAuth initiation error', async () => {
const mockResult = {
success: false,
error: 'OAuth not configured',
};
vi.mocked(api.initiateMicrosoftOAuth).mockResolvedValue(mockResult);
const { result } = renderHook(() => useInitiateMicrosoftOAuth(), {
wrapper: createWrapper(),
});
let mutationResult;
await act(async () => {
mutationResult = await result.current.mutateAsync();
});
expect(mutationResult?.success).toBe(false);
expect(mutationResult?.error).toBe('OAuth not configured');
});
});
describe('useOAuthCredentials', () => {
it('fetches OAuth credentials list', async () => {
const mockCredentials = [
{
id: 1,
provider: 'google' as const,
email: 'support@example.com',
purpose: 'email',
is_valid: true,
is_expired: false,
last_used_at: '2025-01-01T12:00:00Z',
last_error: '',
created_at: '2025-01-01T00:00:00Z',
},
{
id: 2,
provider: 'microsoft' as const,
email: 'admin@example.com',
purpose: 'calendar',
is_valid: false,
is_expired: true,
last_used_at: '2024-12-01T10:00:00Z',
last_error: 'Token expired',
created_at: '2024-11-01T00:00:00Z',
},
];
vi.mocked(api.getOAuthCredentials).mockResolvedValue(mockCredentials);
const { result } = renderHook(() => useOAuthCredentials(), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(api.getOAuthCredentials).toHaveBeenCalledOnce();
expect(result.current.data).toHaveLength(2);
expect(result.current.data?.[0].provider).toBe('google');
expect(result.current.data?.[1].is_expired).toBe(true);
});
it('handles empty credentials list', async () => {
vi.mocked(api.getOAuthCredentials).mockResolvedValue([]);
const { result } = renderHook(() => useOAuthCredentials(), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(result.current.data).toHaveLength(0);
});
});
describe('useDeleteOAuthCredential', () => {
it('deletes OAuth credential', async () => {
const mockResult = {
success: true,
message: 'Credential deleted successfully',
};
vi.mocked(api.deleteOAuthCredential).mockResolvedValue(mockResult);
const { result } = renderHook(() => useDeleteOAuthCredential(), {
wrapper: createWrapper(),
});
let mutationResult;
await act(async () => {
mutationResult = await result.current.mutateAsync(123);
});
expect(api.deleteOAuthCredential).toHaveBeenCalledWith(123);
expect(mutationResult?.success).toBe(true);
});
it('invalidates credentials cache on successful delete', async () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});
const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries');
const mockResult = {
success: true,
message: 'Deleted',
};
vi.mocked(api.deleteOAuthCredential).mockResolvedValue(mockResult);
const wrapper = ({ children }: { children: React.ReactNode }) =>
React.createElement(QueryClientProvider, { client: queryClient }, children);
const { result } = renderHook(() => useDeleteOAuthCredential(), { wrapper });
await act(async () => {
await result.current.mutateAsync(123);
});
expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['oauthCredentials'] });
});
it('handles delete error', async () => {
vi.mocked(api.deleteOAuthCredential).mockRejectedValue(
new Error('Credential not found')
);
const { result } = renderHook(() => useDeleteOAuthCredential(), {
wrapper: createWrapper(),
});
let error;
await act(async () => {
try {
await result.current.mutateAsync(999);
} catch (e) {
error = e;
}
});
expect(error).toBeDefined();
expect(error).toBeInstanceOf(Error);
});
});
});