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 payments API module vi.mock('../../api/payments', () => ({ getPaymentConfig: vi.fn(), getApiKeys: vi.fn(), validateApiKeys: vi.fn(), saveApiKeys: vi.fn(), revalidateApiKeys: vi.fn(), deleteApiKeys: vi.fn(), getConnectStatus: vi.fn(), initiateConnectOnboarding: vi.fn(), refreshConnectOnboardingLink: vi.fn(), })); import { usePaymentConfig, useApiKeys, useValidateApiKeys, useSaveApiKeys, useRevalidateApiKeys, useDeleteApiKeys, useConnectStatus, useConnectOnboarding, useRefreshConnectLink, paymentKeys, } from '../usePayments'; import * as paymentsApi from '../../api/payments'; // Create wrapper with fresh QueryClient for each test 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('usePayments hooks', () => { beforeEach(() => { vi.clearAllMocks(); }); describe('paymentKeys', () => { it('generates correct query keys', () => { expect(paymentKeys.all).toEqual(['payments']); expect(paymentKeys.config()).toEqual(['payments', 'config']); expect(paymentKeys.apiKeys()).toEqual(['payments', 'apiKeys']); expect(paymentKeys.connectStatus()).toEqual(['payments', 'connectStatus']); }); }); describe('usePaymentConfig', () => { it('fetches payment configuration', async () => { const mockConfig = { payment_mode: 'direct_api' as const, tier: 'free', tier_allows_payments: true, stripe_configured: true, can_accept_payments: true, api_keys: { id: 1, status: 'active' as const, secret_key_masked: 'sk_test_****1234', publishable_key_masked: 'pk_test_****5678', last_validated_at: '2025-12-07T10:00:00Z', stripe_account_id: 'acct_123', stripe_account_name: 'Test Business', validation_error: '', created_at: '2025-12-01T10:00:00Z', updated_at: '2025-12-07T10:00:00Z', }, connect_account: null, }; vi.mocked(paymentsApi.getPaymentConfig).mockResolvedValue({ data: mockConfig } as any); const { result } = renderHook(() => usePaymentConfig(), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(paymentsApi.getPaymentConfig).toHaveBeenCalledTimes(1); expect(result.current.data).toEqual(mockConfig); }); it('uses 30 second staleTime', async () => { vi.mocked(paymentsApi.getPaymentConfig).mockResolvedValue({ data: {} } as any); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false }, }, }); const wrapper = ({ children }: { children: React.ReactNode }) => React.createElement(QueryClientProvider, { client: queryClient }, children); renderHook(() => usePaymentConfig(), { wrapper }); await waitFor(() => { const queryState = queryClient.getQueryState(paymentKeys.config()); expect(queryState).toBeDefined(); }); const queryState = queryClient.getQueryState(paymentKeys.config()); expect(queryState?.dataUpdatedAt).toBeDefined(); }); }); describe('useApiKeys', () => { it('fetches current API keys configuration', async () => { const mockApiKeys = { configured: true, id: 1, status: 'active' as const, secret_key_masked: 'sk_test_****1234', publishable_key_masked: 'pk_test_****5678', last_validated_at: '2025-12-07T10:00:00Z', stripe_account_id: 'acct_123', stripe_account_name: 'Test Business', validation_error: '', }; vi.mocked(paymentsApi.getApiKeys).mockResolvedValue({ data: mockApiKeys } as any); const { result } = renderHook(() => useApiKeys(), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(paymentsApi.getApiKeys).toHaveBeenCalledTimes(1); expect(result.current.data).toEqual(mockApiKeys); }); it('handles unconfigured state', async () => { const mockApiKeys = { configured: false, message: 'No API keys configured', }; vi.mocked(paymentsApi.getApiKeys).mockResolvedValue({ data: mockApiKeys } as any); const { result } = renderHook(() => useApiKeys(), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(result.current.data?.configured).toBe(false); expect(result.current.data?.message).toBe('No API keys configured'); }); }); describe('useValidateApiKeys', () => { it('validates API keys successfully', async () => { const mockValidationResult = { valid: true, account_id: 'acct_123', account_name: 'Test Account', environment: 'test', }; vi.mocked(paymentsApi.validateApiKeys).mockResolvedValue({ data: mockValidationResult } as any); const { result } = renderHook(() => useValidateApiKeys(), { wrapper: createWrapper(), }); await act(async () => { const response = await result.current.mutateAsync({ secretKey: 'sk_test_123', publishableKey: 'pk_test_456', }); expect(response).toEqual(mockValidationResult); }); expect(paymentsApi.validateApiKeys).toHaveBeenCalledWith('sk_test_123', 'pk_test_456'); }); it('handles validation failure', async () => { const mockValidationResult = { valid: false, error: 'Invalid API keys', }; vi.mocked(paymentsApi.validateApiKeys).mockResolvedValue({ data: mockValidationResult } as any); const { result } = renderHook(() => useValidateApiKeys(), { wrapper: createWrapper(), }); await act(async () => { const response = await result.current.mutateAsync({ secretKey: 'sk_test_invalid', publishableKey: 'pk_test_invalid', }); expect(response.valid).toBe(false); expect(response.error).toBe('Invalid API keys'); }); }); }); describe('useSaveApiKeys', () => { it('saves API keys successfully', async () => { const mockSavedKeys = { id: 1, status: 'active' as const, secret_key_masked: 'sk_test_****1234', publishable_key_masked: 'pk_test_****5678', last_validated_at: '2025-12-07T10:00:00Z', stripe_account_id: 'acct_123', stripe_account_name: 'Test Business', validation_error: '', created_at: '2025-12-07T10:00:00Z', updated_at: '2025-12-07T10:00:00Z', }; vi.mocked(paymentsApi.saveApiKeys).mockResolvedValue({ data: mockSavedKeys } as any); const { result } = renderHook(() => useSaveApiKeys(), { wrapper: createWrapper(), }); await act(async () => { const response = await result.current.mutateAsync({ secretKey: 'sk_test_123', publishableKey: 'pk_test_456', }); expect(response).toEqual(mockSavedKeys); }); expect(paymentsApi.saveApiKeys).toHaveBeenCalledWith('sk_test_123', 'pk_test_456'); }); it('invalidates payment config and api keys queries on success', async () => { vi.mocked(paymentsApi.saveApiKeys).mockResolvedValue({ data: {} } as any); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false }, }, }); const invalidateQueriesSpy = vi.spyOn(queryClient, 'invalidateQueries'); const wrapper = ({ children }: { children: React.ReactNode }) => React.createElement(QueryClientProvider, { client: queryClient }, children); const { result } = renderHook(() => useSaveApiKeys(), { wrapper }); await act(async () => { await result.current.mutateAsync({ secretKey: 'sk_test_123', publishableKey: 'pk_test_456', }); }); expect(invalidateQueriesSpy).toHaveBeenCalledWith({ queryKey: paymentKeys.config() }); expect(invalidateQueriesSpy).toHaveBeenCalledWith({ queryKey: paymentKeys.apiKeys() }); }); }); describe('useRevalidateApiKeys', () => { it('revalidates stored API keys', async () => { const mockValidationResult = { valid: true, account_id: 'acct_123', account_name: 'Test Account', environment: 'test', }; vi.mocked(paymentsApi.revalidateApiKeys).mockResolvedValue({ data: mockValidationResult } as any); const { result } = renderHook(() => useRevalidateApiKeys(), { wrapper: createWrapper(), }); await act(async () => { const response = await result.current.mutateAsync(); expect(response).toEqual(mockValidationResult); }); expect(paymentsApi.revalidateApiKeys).toHaveBeenCalledTimes(1); }); it('invalidates payment config and api keys queries on success', async () => { vi.mocked(paymentsApi.revalidateApiKeys).mockResolvedValue({ data: { valid: true } } as any); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false }, }, }); const invalidateQueriesSpy = vi.spyOn(queryClient, 'invalidateQueries'); const wrapper = ({ children }: { children: React.ReactNode }) => React.createElement(QueryClientProvider, { client: queryClient }, children); const { result } = renderHook(() => useRevalidateApiKeys(), { wrapper }); await act(async () => { await result.current.mutateAsync(); }); expect(invalidateQueriesSpy).toHaveBeenCalledWith({ queryKey: paymentKeys.config() }); expect(invalidateQueriesSpy).toHaveBeenCalledWith({ queryKey: paymentKeys.apiKeys() }); }); }); describe('useDeleteApiKeys', () => { it('deletes API keys successfully', async () => { const mockDeleteResponse = { success: true, message: 'API keys deleted successfully', }; vi.mocked(paymentsApi.deleteApiKeys).mockResolvedValue({ data: mockDeleteResponse } as any); const { result } = renderHook(() => useDeleteApiKeys(), { wrapper: createWrapper(), }); await act(async () => { const response = await result.current.mutateAsync(); expect(response).toEqual(mockDeleteResponse); }); expect(paymentsApi.deleteApiKeys).toHaveBeenCalledTimes(1); }); it('invalidates payment config and api keys queries on success', async () => { vi.mocked(paymentsApi.deleteApiKeys).mockResolvedValue({ data: { success: true } } as any); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false }, }, }); const invalidateQueriesSpy = vi.spyOn(queryClient, 'invalidateQueries'); const wrapper = ({ children }: { children: React.ReactNode }) => React.createElement(QueryClientProvider, { client: queryClient }, children); const { result } = renderHook(() => useDeleteApiKeys(), { wrapper }); await act(async () => { await result.current.mutateAsync(); }); expect(invalidateQueriesSpy).toHaveBeenCalledWith({ queryKey: paymentKeys.config() }); expect(invalidateQueriesSpy).toHaveBeenCalledWith({ queryKey: paymentKeys.apiKeys() }); }); }); describe('useConnectStatus', () => { it('fetches Connect account status', async () => { const mockConnectStatus = { id: 1, business: 1, business_name: 'Test Business', business_subdomain: 'test', stripe_account_id: 'acct_connect_123', account_type: 'standard' as const, status: 'active' as const, charges_enabled: true, payouts_enabled: true, details_submitted: true, onboarding_complete: true, onboarding_link: null, onboarding_link_expires_at: null, is_onboarding_link_valid: false, created_at: '2025-12-01T10:00:00Z', updated_at: '2025-12-07T10:00:00Z', }; vi.mocked(paymentsApi.getConnectStatus).mockResolvedValue({ data: mockConnectStatus } as any); const { result } = renderHook(() => useConnectStatus(), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(paymentsApi.getConnectStatus).toHaveBeenCalledTimes(1); expect(result.current.data).toEqual(mockConnectStatus); }); it('handles onboarding state with valid link', async () => { const mockConnectStatus = { id: 1, business: 1, business_name: 'Test Business', business_subdomain: 'test', stripe_account_id: 'acct_connect_123', account_type: 'custom' as const, status: 'onboarding' as const, charges_enabled: false, payouts_enabled: false, details_submitted: false, onboarding_complete: false, onboarding_link: 'https://connect.stripe.com/setup/...', onboarding_link_expires_at: '2025-12-08T10:00:00Z', is_onboarding_link_valid: true, created_at: '2025-12-07T10:00:00Z', updated_at: '2025-12-07T10:00:00Z', }; vi.mocked(paymentsApi.getConnectStatus).mockResolvedValue({ data: mockConnectStatus } as any); const { result } = renderHook(() => useConnectStatus(), { wrapper: createWrapper(), }); await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); expect(result.current.data?.onboarding_link).toBe('https://connect.stripe.com/setup/...'); expect(result.current.data?.is_onboarding_link_valid).toBe(true); }); it('is enabled by default', async () => { vi.mocked(paymentsApi.getConnectStatus).mockResolvedValue({ data: {} } as any); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false }, }, }); const wrapper = ({ children }: { children: React.ReactNode }) => React.createElement(QueryClientProvider, { client: queryClient }, children); renderHook(() => useConnectStatus(), { wrapper }); await waitFor(() => { expect(paymentsApi.getConnectStatus).toHaveBeenCalled(); }); }); }); describe('useConnectOnboarding', () => { it('initiates Connect onboarding successfully', async () => { const mockOnboardingResponse = { account_type: 'standard' as const, url: 'https://connect.stripe.com/setup/s/acct_123/abc123', stripe_account_id: 'acct_123', }; vi.mocked(paymentsApi.initiateConnectOnboarding).mockResolvedValue({ data: mockOnboardingResponse } as any); const { result } = renderHook(() => useConnectOnboarding(), { wrapper: createWrapper(), }); await act(async () => { const response = await result.current.mutateAsync({ refreshUrl: 'http://test.lvh.me:5173/payments/refresh', returnUrl: 'http://test.lvh.me:5173/payments/complete', }); expect(response).toEqual(mockOnboardingResponse); }); expect(paymentsApi.initiateConnectOnboarding).toHaveBeenCalledWith( 'http://test.lvh.me:5173/payments/refresh', 'http://test.lvh.me:5173/payments/complete' ); }); it('invalidates payment config and connect status queries on success', async () => { vi.mocked(paymentsApi.initiateConnectOnboarding).mockResolvedValue({ data: { account_type: 'standard', url: 'https://stripe.com' } } as any); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false }, }, }); const invalidateQueriesSpy = vi.spyOn(queryClient, 'invalidateQueries'); const wrapper = ({ children }: { children: React.ReactNode }) => React.createElement(QueryClientProvider, { client: queryClient }, children); const { result } = renderHook(() => useConnectOnboarding(), { wrapper }); await act(async () => { await result.current.mutateAsync({ refreshUrl: 'http://test.lvh.me:5173/refresh', returnUrl: 'http://test.lvh.me:5173/return', }); }); expect(invalidateQueriesSpy).toHaveBeenCalledWith({ queryKey: paymentKeys.config() }); expect(invalidateQueriesSpy).toHaveBeenCalledWith({ queryKey: paymentKeys.connectStatus() }); }); }); describe('useRefreshConnectLink', () => { it('refreshes Connect onboarding link successfully', async () => { const mockRefreshResponse = { url: 'https://connect.stripe.com/setup/s/acct_123/xyz789', }; vi.mocked(paymentsApi.refreshConnectOnboardingLink).mockResolvedValue({ data: mockRefreshResponse } as any); const { result } = renderHook(() => useRefreshConnectLink(), { wrapper: createWrapper(), }); await act(async () => { const response = await result.current.mutateAsync({ refreshUrl: 'http://test.lvh.me:5173/payments/refresh', returnUrl: 'http://test.lvh.me:5173/payments/complete', }); expect(response).toEqual(mockRefreshResponse); }); expect(paymentsApi.refreshConnectOnboardingLink).toHaveBeenCalledWith( 'http://test.lvh.me:5173/payments/refresh', 'http://test.lvh.me:5173/payments/complete' ); }); it('invalidates connect status query on success', async () => { vi.mocked(paymentsApi.refreshConnectOnboardingLink).mockResolvedValue({ data: { url: 'https://stripe.com' } } as any); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false }, }, }); const invalidateQueriesSpy = vi.spyOn(queryClient, 'invalidateQueries'); const wrapper = ({ children }: { children: React.ReactNode }) => React.createElement(QueryClientProvider, { client: queryClient }, children); const { result } = renderHook(() => useRefreshConnectLink(), { wrapper }); await act(async () => { await result.current.mutateAsync({ refreshUrl: 'http://test.lvh.me:5173/refresh', returnUrl: 'http://test.lvh.me:5173/return', }); }); expect(invalidateQueriesSpy).toHaveBeenCalledWith({ queryKey: paymentKeys.connectStatus() }); // Should NOT invalidate config on refresh (only on initial onboarding) expect(invalidateQueriesSpy).not.toHaveBeenCalledWith({ queryKey: paymentKeys.config() }); }); }); });