diff --git a/packages/react/src/components/auth0/my-organization/__tests__/domain-table.test.tsx b/packages/react/src/components/auth0/my-organization/__tests__/domain-table.test.tsx index 1908ddcb2..17417e5f8 100644 --- a/packages/react/src/components/auth0/my-organization/__tests__/domain-table.test.tsx +++ b/packages/react/src/components/auth0/my-organization/__tests__/domain-table.test.tsx @@ -13,8 +13,7 @@ import { createMockCreateAction, createMockVerifyAction, createMockDeleteAction, - createMockLogic, - createMockApi, + createMockDomainTableReturn, } from '@/tests/utils/__mocks__/my-organization/domain-management/domain.mocks'; import { renderWithProviders } from '@/tests/utils/test-provider'; import { mockCore, mockToast } from '@/tests/utils/test-setup'; @@ -681,27 +680,32 @@ describe('DomainTable', () => { }); describe('DomainTableView', () => { - // Provide all required handlers and properties for UseDomainTableResult & DomainTableProps - const logic = createMockLogic(); - const handlers = createMockApi(); + const mockDomainTable = createMockDomainTableReturn(); + const defaultViewProps = { + domainTable: mockDomainTable, + schema: undefined, + styling: { variables: { common: {}, light: {}, dark: {} }, classes: {} }, + hideHeader: false, + readOnly: false, + customMessages: {}, + createAction: undefined, + onOpenProvider: undefined, + onCreateProvider: undefined, + }; it('renders the table and header', () => { - renderWithProviders(); + renderWithProviders(); expect(screen.getByRole('table')).toBeInTheDocument(); expect(screen.getByText(/header.title/i)).toBeInTheDocument(); }); it('does not render header if hideHeader is true', () => { - renderWithProviders( - , - ); + renderWithProviders(); expect(screen.queryByText(/header.title/i)).not.toBeInTheDocument(); }); it('disables create button if readOnly is true', () => { - renderWithProviders( - , - ); + renderWithProviders(); expect(screen.getByRole('button', { name: /create/i })).toBeDisabled(); }); }); diff --git a/packages/react/src/components/auth0/my-organization/domain-table.tsx b/packages/react/src/components/auth0/my-organization/domain-table.tsx index 10d01c95b..97d3a6cb8 100644 --- a/packages/react/src/components/auth0/my-organization/domain-table.tsx +++ b/packages/react/src/components/auth0/my-organization/domain-table.tsx @@ -15,7 +15,6 @@ import { Header } from '@/components/auth0/shared/header'; import { StyledScope } from '@/components/auth0/shared/styled-scope'; import { Badge } from '@/components/ui/badge'; import { useDomainTable } from '@/hooks/my-organization/use-domain-table'; -import { useDomainTableLogic } from '@/hooks/my-organization/use-domain-table-logic'; import { useTheme } from '@/hooks/shared/use-theme'; import { useTranslator } from '@/hooks/shared/use-translator'; import { getStatusBadgeVariant } from '@/lib/utils/my-organization/domain-management/domain-management-utils'; @@ -46,9 +45,7 @@ function DomainTable(props: DomainTableProps) { onCreateProvider, } = props; - const { t } = useTranslator('domain_management', customMessages); - - const domainTableState = useDomainTable({ + const domainTable = useDomainTable({ createAction, verifyAction, deleteAction, @@ -57,46 +54,42 @@ function DomainTable(props: DomainTableProps) { customMessages, }); - const domainTableHandlers = useDomainTableLogic({ - t, - onCreateDomain: domainTableState.onCreateDomain, - onVerifyDomain: domainTableState.onVerifyDomain, - onDeleteDomain: domainTableState.onDeleteDomain, - onAssociateToProvider: domainTableState.onAssociateToProvider, - onDeleteFromProvider: domainTableState.onDeleteFromProvider, - fetchProviders: domainTableState.fetchProviders, - fetchDomains: domainTableState.fetchDomains, - }); - - const domainTableLogic = { - ...domainTableState, - schema, - styling, - hideHeader, - readOnly, - onOpenProvider, - onCreateProvider, - }; - return ( - - + + ); } /** * DomainTableView — Presentational component. - * @param props - View props with logic and handlers + * @param props - View props * @returns Domain table view element * @internal */ function DomainTableView({ - logic, - handlers, -}: DomainTableViewProps & { handlers: ReturnType }) { + domainTable, + schema, + styling, + hideHeader, + readOnly = false, + customMessages, + createAction, + onOpenProvider, + onCreateProvider, +}: DomainTableViewProps) { const { isDarkMode } = useTheme(); - const { t } = useTranslator('domain_management', logic.customMessages); + const { t } = useTranslator('domain_management', customMessages); const { domains, @@ -106,17 +99,6 @@ function DomainTableView({ isFetching, isLoadingProviders, isDeleting, - schema, - styling, - hideHeader, - readOnly = false, - customMessages, - createAction, - onOpenProvider, - onCreateProvider, - } = logic; - - const { showCreateModal, showConfigureModal, showVerifyModal, @@ -135,7 +117,7 @@ function DomainTableView({ handleConfigureClick, handleVerifyClick, handleDeleteClick, - } = handlers; + } = domainTable; const currentStyles = React.useMemo( () => getComponentStyles(styling, isDarkMode), @@ -172,7 +154,7 @@ function DomainTableView({ , -): UseDomainTableLogicOptions => ({ - t: createMockI18nService().translator('my-organization'), - onCreateDomain: vi.fn(), - onVerifyDomain: vi.fn(), - onDeleteDomain: vi.fn(), - onAssociateToProvider: vi.fn(), - onDeleteFromProvider: vi.fn(), - fetchProviders: vi.fn(), - fetchDomains: vi.fn(), - ...overrides, -}); - -// ===== Tests ===== - -describe('useDomainTableLogic', () => { - let mockCoreClient: ReturnType; - let mockHandleError: ReturnType; - let mockOptions: UseDomainTableLogicOptions; - - beforeEach(() => { - vi.clearAllMocks(); - - mockCoreClient = initMockCoreClient(); - mockHandleError = vi.fn(); - mockOptions = createMockOptions(); - - vi.spyOn(useCoreClientModule, 'useCoreClient').mockReturnValue({ - coreClient: mockCoreClient, - }); - - vi.spyOn(useErrorHandlerModule, 'useErrorHandler').mockReturnValue(mockHandleError); - }); - - describe('Initial State', () => { - it('should initialize with correct default state', () => { - const { result } = renderHook(() => useDomainTableLogic(mockOptions)); - - expect(result.current.showCreateModal).toBe(false); - expect(result.current.showConfigureModal).toBe(false); - expect(result.current.showVerifyModal).toBe(false); - expect(result.current.showDeleteModal).toBe(false); - expect(result.current.verifyError).toBeUndefined(); - expect(result.current.selectedDomain).toBeNull(); - }); - - it('should call fetchDomains on mount when coreClient is available', async () => { - renderHook(() => useDomainTableLogic(mockOptions)); - - await waitFor(() => { - expect(mockOptions.fetchDomains).toHaveBeenCalledTimes(1); - }); - }); - - it('should handle fetchDomains error on initialization', async () => { - const error = new Error('Fetch domains failed'); - const mockFetchDomains = vi.fn().mockImplementation(() => { - throw error; - }); - const options = createMockOptions({ fetchDomains: mockFetchDomains }); - - renderHook(() => useDomainTableLogic(options)); - - await waitFor(() => { - expect(mockHandleError).toHaveBeenCalledWith(error, { - fallbackMessage: 'domain_table.notifications.fetch_domains_error', - }); - }); - }); - }); - - describe('Modal State Management', () => { - it('should update create modal state', () => { - const { result } = renderHook(() => useDomainTableLogic(mockOptions)); - - act(() => { - result.current.setShowCreateModal(true); - }); - - expect(result.current.showCreateModal).toBe(true); - }); - - it('should update configure modal state', () => { - const { result } = renderHook(() => useDomainTableLogic(mockOptions)); - - act(() => { - result.current.setShowConfigureModal(true); - }); - - expect(result.current.showConfigureModal).toBe(true); - }); - - it('should update verify modal state', () => { - const { result } = renderHook(() => useDomainTableLogic(mockOptions)); - - act(() => { - result.current.setShowVerifyModal(true); - }); - - expect(result.current.showVerifyModal).toBe(true); - }); - - it('should update delete modal state', () => { - const { result } = renderHook(() => useDomainTableLogic(mockOptions)); - - act(() => { - result.current.setShowDeleteModal(true); - }); - - expect(result.current.showDeleteModal).toBe(true); - }); - }); - - describe('handleCreate', () => { - it('should create domain successfully and show verify modal', async () => { - const mockDomain = createMockDomain({ domain: 'test.com' }); - const mockOnCreateDomain = vi.fn().mockResolvedValue(mockDomain); - const options = createMockOptions({ onCreateDomain: mockOnCreateDomain }); - - const { result } = renderHook(() => useDomainTableLogic(options)); - - await act(async () => { - await result.current.handleCreate('test.com'); - }); - - expect(mockOnCreateDomain).toHaveBeenCalledWith({ domain: 'test.com' }); - expect(mockedShowToast).toHaveBeenCalledWith({ - type: 'success', - message: 'domain_table.notifications.domain_create.success', - }); - expect(result.current.selectedDomain).toEqual(mockDomain); - expect(result.current.showCreateModal).toBe(false); - expect(result.current.showVerifyModal).toBe(true); - }); - - it('should handle create domain error', async () => { - const error = new Error('Create failed'); - const mockOnCreateDomain = vi.fn().mockRejectedValue(error); - const options = createMockOptions({ onCreateDomain: mockOnCreateDomain }); - - const { result } = renderHook(() => useDomainTableLogic(options)); - - await act(async () => { - await result.current.handleCreate('test.com'); - }); - - expect(mockHandleError).toHaveBeenCalledWith(error, { - fallbackMessage: 'domain_table.notifications.domain_create.error', - }); - }); - }); - - describe('handleVerify', () => { - it('should verify domain successfully and close verify modal', async () => { - const mockDomain = createMockDomain(); - const mockOnVerifyDomain = vi.fn().mockResolvedValue(true); - const options = createMockOptions({ onVerifyDomain: mockOnVerifyDomain }); - - const { result } = renderHook(() => useDomainTableLogic(options)); - - await act(async () => { - await result.current.handleVerify(mockDomain); - }); - - expect(mockOnVerifyDomain).toHaveBeenCalledWith(mockDomain); - expect(result.current.showVerifyModal).toBe(false); - expect(mockedShowToast).toHaveBeenCalledWith({ - type: 'success', - message: 'domain_table.notifications.domain_verify.success', - }); - }); - - it('should handle verification failure and set verify error', async () => { - const mockDomain = createMockDomain({ domain: 'test.com' }); - const mockOnVerifyDomain = vi.fn().mockResolvedValue(false); - const options = createMockOptions({ onVerifyDomain: mockOnVerifyDomain }); - - const { result } = renderHook(() => useDomainTableLogic(options)); - - await act(async () => { - await result.current.handleVerify(mockDomain); - }); - - expect(result.current.verifyError).toBe('domain_verify.modal.errors.verification_failed'); - }); - - it('should handle verify domain error', async () => { - const mockDomain = createMockDomain(); - const error = new Error('Verify failed'); - const mockOnVerifyDomain = vi.fn().mockRejectedValue(error); - const options = createMockOptions({ onVerifyDomain: mockOnVerifyDomain }); - - const { result } = renderHook(() => useDomainTableLogic(options)); - - await act(async () => { - await result.current.handleVerify(mockDomain); - }); - - expect(mockHandleError).toHaveBeenCalledWith(error, { - fallbackMessage: 'domain_table.notifications.domain_verify.error', - }); - }); - }); - - describe('handleDelete', () => { - it('should delete domain successfully', async () => { - const mockDomain = createMockDomain({ domain: 'test.com' }); - const mockOnDeleteDomain = vi.fn().mockResolvedValue(undefined); - const options = createMockOptions({ onDeleteDomain: mockOnDeleteDomain }); - - const { result } = renderHook(() => useDomainTableLogic(options)); - - await act(async () => { - await result.current.handleDelete(mockDomain); - }); - - expect(mockOnDeleteDomain).toHaveBeenCalledWith(mockDomain); - expect(mockedShowToast).toHaveBeenCalledWith({ - type: 'success', - message: 'domain_table.notifications.domain_delete.success', - }); - expect(result.current.showDeleteModal).toBe(false); - expect(result.current.showVerifyModal).toBe(false); - }); - - it('should handle delete domain error', async () => { - const mockDomain = createMockDomain(); - const error = new Error('Delete failed'); - const mockOnDeleteDomain = vi.fn().mockRejectedValue(error); - const options = createMockOptions({ onDeleteDomain: mockOnDeleteDomain }); - - const { result } = renderHook(() => useDomainTableLogic(options)); - - await act(async () => { - await result.current.handleDelete(mockDomain); - }); - - expect(mockHandleError).toHaveBeenCalledWith(error, { - fallbackMessage: 'domain_table.notifications.domain_delete.error', - }); - }); - }); - - describe('handleToggleSwitch', () => { - it('should associate domain to provider when checked is true', async () => { - const mockDomain = createMockDomain({ domain: 'test.com' }); - const mockProvider = createMockIdentityProvider({ name: 'TestIDP' }); - const mockOnAssociateToProvider = vi.fn().mockResolvedValue(undefined); - const options = createMockOptions({ onAssociateToProvider: mockOnAssociateToProvider }); - - const { result } = renderHook(() => useDomainTableLogic(options)); - - await act(async () => { - await result.current.handleToggleSwitch(mockDomain, mockProvider, true); - }); - - expect(mockOnAssociateToProvider).toHaveBeenCalledWith(mockDomain, mockProvider); - expect(mockedShowToast).toHaveBeenCalledWith({ - type: 'success', - message: 'domain_table.notifications.domain_associate_provider.success', - }); - }); - - it('should delete domain from provider when checked is false', async () => { - const mockDomain = createMockDomain({ domain: 'test.com' }); - const mockProvider = createMockIdentityProvider({ name: 'TestIDP' }); - const mockOnDeleteFromProvider = vi.fn().mockResolvedValue(undefined); - const options = createMockOptions({ onDeleteFromProvider: mockOnDeleteFromProvider }); - - const { result } = renderHook(() => useDomainTableLogic(options)); - - await act(async () => { - await result.current.handleToggleSwitch(mockDomain, mockProvider, false); - }); - - expect(mockOnDeleteFromProvider).toHaveBeenCalledWith(mockDomain, mockProvider); - expect(mockedShowToast).toHaveBeenCalledWith({ - type: 'success', - message: 'domain_table.notifications.domain_delete_provider.success', - }); - }); - - it('should handle associate to provider error', async () => { - const mockDomain = createMockDomain(); - const mockProvider = createMockIdentityProvider(); - const error = new Error('Associate failed'); - const mockOnAssociateToProvider = vi.fn().mockRejectedValue(error); - const options = createMockOptions({ onAssociateToProvider: mockOnAssociateToProvider }); - - const { result } = renderHook(() => useDomainTableLogic(options)); - - await act(async () => { - await result.current.handleToggleSwitch(mockDomain, mockProvider, true); - }); - - expect(mockHandleError).toHaveBeenCalledWith(error, { - fallbackMessage: 'domain_table.notifications.domain_associate_provider.error', - }); - }); - - it('should handle delete from provider error', async () => { - const mockDomain = createMockDomain(); - const mockProvider = createMockIdentityProvider(); - const error = new Error('Delete from provider failed'); - const mockOnDeleteFromProvider = vi.fn().mockRejectedValue(error); - const options = createMockOptions({ onDeleteFromProvider: mockOnDeleteFromProvider }); - - const { result } = renderHook(() => useDomainTableLogic(options)); - - await act(async () => { - await result.current.handleToggleSwitch(mockDomain, mockProvider, false); - }); - - expect(mockHandleError).toHaveBeenCalledWith(error, { - fallbackMessage: 'domain_table.notifications.domain_delete_provider.error', - }); - }); - }); - - describe('handleCloseVerifyModal', () => { - it('should close verify modal and clear verify error', async () => { - const { result } = renderHook(() => useDomainTableLogic(mockOptions)); - - // Set initial state - act(() => { - result.current.setShowVerifyModal(true); - }); - - // Simulate verify error - await act(async () => { - await result.current.handleVerify(createMockDomain()); - }); - - // Close modal - act(() => { - result.current.handleCloseVerifyModal(); - }); - - expect(result.current.showVerifyModal).toBe(false); - expect(result.current.verifyError).toBeUndefined(); - }); - }); - - describe('handleCreateClick', () => { - it('should show create modal', () => { - const { result } = renderHook(() => useDomainTableLogic(mockOptions)); - - act(() => { - result.current.handleCreateClick(); - }); - - expect(result.current.showCreateModal).toBe(true); - }); - }); - - describe('handleConfigureClick', () => { - it('should show verify modal for unverified domain', async () => { - const mockDomain = createMockDomain({ status: 'pending' }); - const { result } = renderHook(() => useDomainTableLogic(mockOptions)); - - await act(async () => { - await result.current.handleConfigureClick(mockDomain); - }); - - expect(result.current.selectedDomain).toEqual(mockDomain); - expect(result.current.showVerifyModal).toBe(true); - }); - - it('should fetch providers and show configure modal for verified domain', async () => { - const mockDomain = createMockDomain({ status: 'verified' }); - const mockFetchProviders = vi.fn().mockResolvedValue(undefined); - const options = createMockOptions({ fetchProviders: mockFetchProviders }); - - const { result } = renderHook(() => useDomainTableLogic(options)); - - await act(async () => { - await result.current.handleConfigureClick(mockDomain); - }); - - expect(result.current.selectedDomain).toEqual(mockDomain); - expect(mockFetchProviders).toHaveBeenCalledWith(mockDomain); - expect(result.current.showConfigureModal).toBe(true); - }); - - it('should handle fetchProviders error for verified domain', async () => { - const mockDomain = createMockDomain({ status: 'verified' }); - const error = new Error('Fetch providers failed'); - const mockFetchProviders = vi.fn().mockRejectedValue(error); - const options = createMockOptions({ fetchProviders: mockFetchProviders }); - - const { result } = renderHook(() => useDomainTableLogic(options)); - - await act(async () => { - await result.current.handleConfigureClick(mockDomain); - }); - - expect(mockHandleError).toHaveBeenCalledWith(error, { - fallbackMessage: 'domain_table.notifications.fetch_providers_error', - }); - }); - }); - - describe('handleVerifyClick', () => { - it('should verify domain, fetch providers, and show configure modal on success', async () => { - const mockDomain = createMockDomain({ domain: 'test.com' }); - const mockOnVerifyDomain = vi.fn().mockResolvedValue(true); - const mockFetchProviders = vi.fn().mockResolvedValue(undefined); - const options = createMockOptions({ - onVerifyDomain: mockOnVerifyDomain, - fetchProviders: mockFetchProviders, - }); - - const { result } = renderHook(() => useDomainTableLogic(options)); - - await act(async () => { - await result.current.handleVerifyClick(mockDomain); - }); - - expect(result.current.selectedDomain).toEqual(mockDomain); - expect(mockOnVerifyDomain).toHaveBeenCalledWith(mockDomain); - expect(mockFetchProviders).toHaveBeenCalledWith(mockDomain); - expect(result.current.showConfigureModal).toBe(true); - expect(mockedShowToast).toHaveBeenCalledWith({ - type: 'success', - message: 'domain_table.notifications.domain_verify.success', - }); - }); - - it('should show error toast on verification failure', async () => { - const mockDomain = createMockDomain({ domain: 'test.com' }); - const mockOnVerifyDomain = vi.fn().mockResolvedValue(false); - const options = createMockOptions({ onVerifyDomain: mockOnVerifyDomain }); - - const { result } = renderHook(() => useDomainTableLogic(options)); - - await act(async () => { - await result.current.handleVerifyClick(mockDomain); - }); - - expect(mockedShowToast).toHaveBeenCalledWith({ - type: 'error', - message: 'domain_table.notifications.domain_verify.verification_failed', - }); - }); - - it('should handle verify click error', async () => { - const mockDomain = createMockDomain(); - const error = new Error('Verify click failed'); - const mockOnVerifyDomain = vi.fn().mockRejectedValue(error); - const options = createMockOptions({ onVerifyDomain: mockOnVerifyDomain }); - - const { result } = renderHook(() => useDomainTableLogic(options)); - - await act(async () => { - await result.current.handleVerifyClick(mockDomain); - }); - - expect(mockHandleError).toHaveBeenCalledWith(error, { - fallbackMessage: 'domain_table.notifications.domain_verify.error', - }); - }); - }); - - describe('handleDeleteClick', () => { - it('should set selected domain and show delete modal', () => { - const mockDomain = createMockDomain(); - const { result } = renderHook(() => useDomainTableLogic(mockOptions)); - - // Set verify modal to true initially - act(() => { - result.current.setShowVerifyModal(true); - }); - - act(() => { - result.current.handleDeleteClick(mockDomain); - }); - - expect(result.current.selectedDomain).toEqual(mockDomain); - expect(result.current.showVerifyModal).toBe(false); - expect(result.current.showDeleteModal).toBe(true); - }); - }); - - describe('Edge Cases and Integration', () => { - it('should handle multiple modal state changes correctly', () => { - const { result } = renderHook(() => useDomainTableLogic(mockOptions)); - - act(() => { - result.current.setShowCreateModal(true); - result.current.setShowConfigureModal(true); - result.current.setShowVerifyModal(true); - result.current.setShowDeleteModal(true); - }); - - expect(result.current.showCreateModal).toBe(true); - expect(result.current.showConfigureModal).toBe(true); - expect(result.current.showVerifyModal).toBe(true); - expect(result.current.showDeleteModal).toBe(true); - - act(() => { - result.current.setShowCreateModal(false); - result.current.setShowConfigureModal(false); - result.current.setShowVerifyModal(false); - result.current.setShowDeleteModal(false); - }); - - expect(result.current.showCreateModal).toBe(false); - expect(result.current.showConfigureModal).toBe(false); - expect(result.current.showVerifyModal).toBe(false); - expect(result.current.showDeleteModal).toBe(false); - }); - - it('should handle domain creation with null return value', async () => { - const mockOnCreateDomain = vi.fn().mockResolvedValue(null); - const options = createMockOptions({ onCreateDomain: mockOnCreateDomain }); - - const { result } = renderHook(() => useDomainTableLogic(options)); - - await act(async () => { - await result.current.handleCreate('test.com'); - }); - - expect(result.current.selectedDomain).toBeNull(); - expect(result.current.showCreateModal).toBe(false); - expect(result.current.showVerifyModal).toBe(true); - }); - - it('should handle various domain statuses in handleConfigureClick', async () => { - const { result } = renderHook(() => useDomainTableLogic(mockOptions)); - - // Test with 'failed' status - const failedDomain = createMockDomain({ status: 'failed' }); - await act(async () => { - await result.current.handleConfigureClick(failedDomain); - }); - expect(result.current.showVerifyModal).toBe(true); - - // Reset state - act(() => { - result.current.setShowVerifyModal(false); - }); - - // Test with 'verified' status - const verifiedDomain = createMockDomain({ status: 'verified' }); - await act(async () => { - await result.current.handleConfigureClick(verifiedDomain); - }); - expect(result.current.showConfigureModal).toBe(true); - }); - }); - - describe('Callback Dependencies', () => { - it('should update callbacks when dependencies change', () => { - const { result, rerender } = renderHook((options) => useDomainTableLogic(options), { - initialProps: mockOptions, - }); - - const initialHandleCreate = result.current.handleCreate; - - // Update the options with a new onCreateDomain function - const newOptions = createMockOptions({ - onCreateDomain: vi.fn(), - }); - - rerender(newOptions); - - // The callback should be different due to dependency change - expect(result.current.handleCreate).not.toBe(initialHandleCreate); - }); - }); -}); diff --git a/packages/react/src/hooks/my-organization/__tests__/use-domain-table.test.ts b/packages/react/src/hooks/my-organization/__tests__/use-domain-table.test.ts index 2ea12ea98..e80a5767c 100644 --- a/packages/react/src/hooks/my-organization/__tests__/use-domain-table.test.ts +++ b/packages/react/src/hooks/my-organization/__tests__/use-domain-table.test.ts @@ -1,837 +1,407 @@ -import type { - CreateOrganizationDomainRequestContent, - EnhancedTranslationFunction, -} from '@auth0/universal-components-core'; -import { BusinessError } from '@auth0/universal-components-core'; -import { renderHook, waitFor } from '@testing-library/react'; -import { describe, it, expect, beforeEach, vi } from 'vitest'; - -import { useDomainTable } from '@/hooks/my-organization/use-domain-table'; -import * as useCoreClientModule from '@/hooks/shared/use-core-client'; -import * as useTranslatorModule from '@/hooks/shared/use-translator'; -import { - mockCore, - createMockDomain, - createMockIdentityProvider, - createMockI18nService, -} from '@/tests/utils'; -import { createTestQueryClientWrapper } from '@/tests/utils/test-provider'; -import type { UseDomainTableOptions } from '@/types/my-organization/domain-management/domain-table-types'; - -// ===== Mock packages ===== - -const { initMockCoreClient } = mockCore(); - -// ===== Mock Data ===== - -const createMockOptions = (overrides?: Partial): UseDomainTableOptions => ({ - createAction: { - onBefore: vi.fn().mockReturnValue(true), - onAfter: vi.fn(), - }, - deleteAction: { - onBefore: vi.fn().mockReturnValue(true), - onAfter: vi.fn(), - }, - verifyAction: { - onBefore: vi.fn().mockReturnValue(true), - onAfter: vi.fn(), - }, - associateToProviderAction: { - onBefore: vi.fn().mockReturnValue(true), - onAfter: vi.fn(), - }, - deleteFromProviderAction: { - onBefore: vi.fn().mockReturnValue(true), - onAfter: vi.fn(), - }, - customMessages: {}, - ...overrides, -}); +import { renderHook, act } from '@testing-library/react'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; -const renderUseDomainTable = (options: UseDomainTableOptions) => { - const { wrapper, queryClient } = createTestQueryClientWrapper(); - return { - queryClient, - ...renderHook(() => useDomainTable(options), { wrapper }), - }; -}; +import { useDomainTable } from '../use-domain-table'; -// ===== Tests ===== +import { useDomainTableService } from '@/hooks/my-organization/shared/services/use-domain-table-service'; +import { mockToast, createMockDomainTableServiceReturn } from '@/tests/utils'; -describe('useDomainTable', () => { - let mockCoreClient: ReturnType; - let mockOptions: UseDomainTableOptions; - let mockT: EnhancedTranslationFunction; +const mockHandleError = vi.fn(); - beforeEach(() => { - vi.clearAllMocks(); +vi.mock('@/hooks/shared/use-translator', () => ({ + useTranslator: () => ({ t: (key: string) => key }), +})); - mockCoreClient = initMockCoreClient(); - mockOptions = createMockOptions(); - mockT = createMockI18nService().translator('my-organization'); +vi.mock('@/hooks/shared/use-error-handler', () => ({ + useErrorHandler: () => mockHandleError, +})); - vi.spyOn(useCoreClientModule, 'useCoreClient').mockReturnValue({ - coreClient: mockCoreClient, - }); +mockToast(); - vi.spyOn(useTranslatorModule, 'useTranslator').mockReturnValue({ - t: mockT, - changeLanguage: vi.fn(), - currentLanguage: 'en', - fallbackLanguage: 'en', - }); - }); - - describe('Initial State', () => { - it('should initialize with correct default state', async () => { - const { result } = renderUseDomainTable(mockOptions); - - // Initial state before query completes - expect(result.current.domains).toEqual([]); - expect(result.current.providers).toEqual([]); - expect(result.current.isCreating).toBe(false); - expect(result.current.isDeleting).toBe(false); - expect(result.current.isVerifying).toBe(false); - expect(result.current.isLoadingProviders).toBe(false); - - // Wait for initial query to complete - await waitFor(() => { - expect(result.current.isFetching).toBe(false); - }); - }); +vi.mock('@/hooks/my-organization/shared/services/use-domain-table-service', () => ({ + useDomainTableService: vi.fn(), +})); - it('should provide all expected functions', () => { - const { result } = renderUseDomainTable(mockOptions); - - expect(typeof result.current.fetchDomains).toBe('function'); - expect(typeof result.current.fetchProviders).toBe('function'); - expect(typeof result.current.onCreateDomain).toBe('function'); - expect(typeof result.current.onVerifyDomain).toBe('function'); - expect(typeof result.current.onDeleteDomain).toBe('function'); - expect(typeof result.current.onAssociateToProvider).toBe('function'); - expect(typeof result.current.onDeleteFromProvider).toBe('function'); - }); - }); +const mockUseDomainTableService = vi.mocked(useDomainTableService); - describe('fetchDomains', () => { - it('should fetch domains successfully', async () => { - const { result } = renderUseDomainTable(mockOptions); - - await result.current.fetchDomains(); +describe('useDomainTable', () => { + const mockDomain = { + id: 'domain_abc123', + org_id: 'org_123', + domain: 'test.com', + status: 'pending' as const, + verification_txt: 'txt', + verification_host: 'host', + }; - await waitFor(() => { - expect(result.current.isFetching).toBe(false); - }); + const verifiedDomain = { ...mockDomain, status: 'verified' as const }; - expect( - mockCoreClient.getMyOrganizationApiClient().organization.domains.list, - ).toHaveBeenCalled(); - }); + const mockProvider = { + id: 'con_abc123', + name: 'TestIDP', + display_name: 'Test IDP', + options: {}, + strategy: 'waad' as const, + }; - it('should handle fetchDomains error and reset loading state', async () => { - const error = new Error('Network error'); - mockCoreClient.getMyOrganizationApiClient().organization.domains.list = vi - .fn() - .mockRejectedValue(error); + const defaultOptions = { + createAction: { onBefore: vi.fn(() => true), onAfter: vi.fn() }, + deleteAction: { onBefore: vi.fn(() => true), onAfter: vi.fn() }, + verifyAction: { onBefore: vi.fn(() => true), onAfter: vi.fn() }, + associateToProviderAction: { onBefore: vi.fn(() => true), onAfter: vi.fn() }, + deleteFromProviderAction: { onBefore: vi.fn(() => true), onAfter: vi.fn() }, + customMessages: {}, + }; - const { result } = renderUseDomainTable(mockOptions); + beforeEach(() => { + vi.clearAllMocks(); + mockUseDomainTableService.mockReturnValue(createMockDomainTableServiceReturn()); + }); - await result.current.fetchDomains(); + it('should return correct initial state', () => { + const { result } = renderHook(() => useDomainTable(defaultOptions)); + + expect(result.current.showCreateModal).toBe(false); + expect(result.current.showConfigureModal).toBe(false); + expect(result.current.showVerifyModal).toBe(false); + expect(result.current.showDeleteModal).toBe(false); + expect(result.current.verifyError).toBeUndefined(); + expect(result.current.selectedDomain).toBeNull(); + expect(result.current.domains).toEqual([]); + expect(result.current.providers).toEqual([]); + }); - await waitFor(() => { - expect(result.current.isFetching).toBe(false); - }); + it('should show create modal on handleCreateClick', () => { + const { result } = renderHook(() => useDomainTable(defaultOptions)); - expect(result.current.isFetching).toBe(false); + act(() => { + result.current.handleCreateClick(); }); - it('should handle empty domains response', async () => { - const { result } = renderUseDomainTable(mockOptions); + expect(result.current.showCreateModal).toBe(true); + }); - await result.current.fetchDomains(); + it('should create domain, show toast, and open verify modal on handleCreate', async () => { + const mockOnCreateDomain = vi.fn().mockResolvedValue(mockDomain); + mockUseDomainTableService.mockReturnValue( + createMockDomainTableServiceReturn({ onCreateDomain: mockOnCreateDomain }), + ); - await waitFor(() => { - expect(result.current.isFetching).toBe(false); - }); + const { result } = renderHook(() => useDomainTable(defaultOptions)); - expect(result.current.domains).toEqual([]); + await act(async () => { + await result.current.handleCreate('test.com'); }); - it('should read from cache without refetching when fetchDomains is called', async () => { - const { result } = renderUseDomainTable(mockOptions); - - // Wait for initial fetch to complete - await waitFor(() => { - expect(result.current.isFetching).toBe(false); - }); + expect(mockOnCreateDomain).toHaveBeenCalledWith({ domain: 'test.com' }); + expect(result.current.selectedDomain).toEqual(mockDomain); + expect(result.current.showCreateModal).toBe(false); + expect(result.current.showVerifyModal).toBe(true); + }); - const initialCallCount = vi.mocked( - mockCoreClient.getMyOrganizationApiClient().organization.domains.list, - ).mock.calls.length; + it('should handle create error on handleCreate', async () => { + const error = new Error('Create failed'); + const mockOnCreateDomain = vi.fn().mockRejectedValue(error); + mockUseDomainTableService.mockReturnValue( + createMockDomainTableServiceReturn({ onCreateDomain: mockOnCreateDomain }), + ); - // Call fetchDomains - should read from cache without triggering refetch - await result.current.fetchDomains(); + const { result } = renderHook(() => useDomainTable(defaultOptions)); - // Should not trigger additional API calls - expect( - vi.mocked(mockCoreClient.getMyOrganizationApiClient().organization.domains.list).mock.calls - .length, - ).toBe(initialCallCount); + await act(async () => { + await result.current.handleCreate('test.com'); }); - it('should refetch when data is invalidated', async () => { - const { result, queryClient } = renderUseDomainTable(mockOptions); - - // Wait for initial fetch to complete - await waitFor(() => { - expect(result.current.isFetching).toBe(false); - }); - - const initialCallCount = vi.mocked( - mockCoreClient.getMyOrganizationApiClient().organization.domains.list, - ).mock.calls.length; - - // Invalidate the query - await queryClient.invalidateQueries({ queryKey: ['domains', 'list'] }); - - // Call fetchDomains - await result.current.fetchDomains(); - - // Should call the API again due to invalidation - await waitFor(() => { - expect( - vi.mocked(mockCoreClient.getMyOrganizationApiClient().organization.domains.list).mock - .calls.length, - ).toBeGreaterThan(initialCallCount); - }); + expect(mockHandleError).toHaveBeenCalledWith(error, { + fallbackMessage: 'domain_table.notifications.domain_create.error', }); }); - describe('fetchProviders', () => { - it('should fetch providers with correct association status', async () => { - const mockDomain = createMockDomain(); - const provider1 = createMockIdentityProvider({ - id: 'provider-1', - display_name: 'Provider 1', - domains: [mockDomain.domain], - }); - const provider2 = createMockIdentityProvider({ - id: 'provider-2', - display_name: 'Provider 2', - domains: [], - }); - const provider3 = createMockIdentityProvider({ - id: 'provider-3', - display_name: 'Provider 3', - domains: [mockDomain.domain], - }); - - // Mock all providers response - domains field indicates association - mockCoreClient.getMyOrganizationApiClient().organization.identityProviders.list = vi - .fn() - .mockResolvedValue({ - identity_providers: [provider1, provider2, provider3], - }); - - const { result } = renderUseDomainTable(mockOptions); - - await result.current.fetchProviders(mockDomain); - - await waitFor(() => { - expect(result.current.isLoadingProviders).toBe(false); - }); - - expect( - mockCoreClient.getMyOrganizationApiClient().organization.identityProviders.list, - ).toHaveBeenCalled(); - - // Verify the providers are correctly matched with association status - expect(result.current.providers).toHaveLength(3); - - // Provider 1 should be associated - const resultProvider1 = result.current.providers.find((p) => p.id === 'provider-1'); - expect(resultProvider1).toBeDefined(); - expect(resultProvider1!.is_associated).toBe(true); - expect(resultProvider1!.display_name).toBe('Provider 1'); - - // Provider 2 should NOT be associated - const resultProvider2 = result.current.providers.find((p) => p.id === 'provider-2'); - expect(resultProvider2).toBeDefined(); - expect(resultProvider2!.is_associated).toBe(false); - expect(resultProvider2!.display_name).toBe('Provider 2'); - - // Provider 3 should be associated - const resultProvider3 = result.current.providers.find((p) => p.id === 'provider-3'); - expect(resultProvider3).toBeDefined(); - expect(resultProvider3!.is_associated).toBe(true); - expect(resultProvider3!.display_name).toBe('Provider 3'); - }); + it('should verify domain and close verify modal on handleVerify success', async () => { + const mockOnVerifyDomain = vi.fn().mockResolvedValue(true); + mockUseDomainTableService.mockReturnValue( + createMockDomainTableServiceReturn({ onVerifyDomain: mockOnVerifyDomain }), + ); - it('should handle providers with no associations', async () => { - const mockDomain = createMockDomain(); - const provider1 = createMockIdentityProvider({ - id: 'provider-1', - display_name: 'Provider 1', - domains: [], - }); - const provider2 = createMockIdentityProvider({ - id: 'provider-2', - display_name: 'Provider 2', - domains: [], - }); - - // Mock all providers response - no domains associated - mockCoreClient.getMyOrganizationApiClient().organization.identityProviders.list = vi - .fn() - .mockResolvedValue({ - identity_providers: [provider1, provider2], - }); - - const { result } = renderUseDomainTable(mockOptions); - - await result.current.fetchProviders(mockDomain); - - await waitFor(() => { - expect(result.current.isLoadingProviders).toBe(false); - }); - - // All providers should have is_associated = false - expect(result.current.providers).toHaveLength(2); - result.current.providers.forEach((provider) => { - expect(provider.is_associated).toBe(false); - }); - }); + const { result } = renderHook(() => useDomainTable(defaultOptions)); - it('should handle all providers being associated', async () => { - const mockDomain = createMockDomain(); - const provider1 = createMockIdentityProvider({ - id: 'provider-1', - display_name: 'Provider 1', - domains: [mockDomain.domain], - }); - const provider2 = createMockIdentityProvider({ - id: 'provider-2', - display_name: 'Provider 2', - domains: [mockDomain.domain], - }); - - // Mock all providers response - all associated via domains field - mockCoreClient.getMyOrganizationApiClient().organization.identityProviders.list = vi - .fn() - .mockResolvedValue({ - identity_providers: [provider1, provider2], - }); - - const { result } = renderUseDomainTable(mockOptions); - - await result.current.fetchProviders(mockDomain); - - await waitFor(() => { - expect(result.current.isLoadingProviders).toBe(false); - }); - - // All providers should have is_associated = true - expect(result.current.providers).toHaveLength(2); - result.current.providers.forEach((provider) => { - expect(provider.is_associated).toBe(true); - }); + await act(async () => { + await result.current.handleVerify(mockDomain); }); - it('should handle fetchProviders error and reset loading state', async () => { - const mockDomain = createMockDomain(); - const error = new Error('Network error'); - mockCoreClient.getMyOrganizationApiClient().organization.identityProviders.list = vi - .fn() - .mockRejectedValue(error); + expect(mockOnVerifyDomain).toHaveBeenCalledWith(mockDomain); + expect(result.current.showVerifyModal).toBe(false); + }); - const { result } = renderUseDomainTable(mockOptions); + it('should set verify error on handleVerify failure', async () => { + const mockOnVerifyDomain = vi.fn().mockResolvedValue(false); + mockUseDomainTableService.mockReturnValue( + createMockDomainTableServiceReturn({ onVerifyDomain: mockOnVerifyDomain }), + ); - await expect(result.current.fetchProviders(mockDomain)).rejects.toThrow('Network error'); + const { result } = renderHook(() => useDomainTable(defaultOptions)); - await waitFor(() => { - expect(result.current.isLoadingProviders).toBe(false); - }); + await act(async () => { + await result.current.handleVerify(mockDomain); }); - it('should handle null/undefined responses gracefully', async () => { - const mockDomain = createMockDomain(); - - // Mock null response - mockCoreClient.getMyOrganizationApiClient().organization.identityProviders.list = vi - .fn() - .mockResolvedValue({ - identity_providers: null, - }); - - const { result } = renderUseDomainTable(mockOptions); + expect(result.current.verifyError).toBe('domain_verify.modal.errors.verification_failed'); + }); - await result.current.fetchProviders(mockDomain); + it('should handle verify error on handleVerify', async () => { + const error = new Error('Verify failed'); + const mockOnVerifyDomain = vi.fn().mockRejectedValue(error); + mockUseDomainTableService.mockReturnValue( + createMockDomainTableServiceReturn({ onVerifyDomain: mockOnVerifyDomain }), + ); - await waitFor(() => { - expect(result.current.isLoadingProviders).toBe(false); - }); + const { result } = renderHook(() => useDomainTable(defaultOptions)); - // Should handle null gracefully and return empty array - expect(result.current.providers).toEqual([]); + await act(async () => { + await result.current.handleVerify(mockDomain); }); - it('should use ensureQueryData to fetch providers', async () => { - const mockDomain = createMockDomain(); - const provider1 = createMockIdentityProvider({ - id: 'provider-1', - display_name: 'Provider 1', - domains: [mockDomain.domain], - }); - - mockCoreClient.getMyOrganizationApiClient().organization.identityProviders.list = vi - .fn() - .mockResolvedValue({ - identity_providers: [provider1], - }); - - const { result } = renderUseDomainTable(mockOptions); - - await result.current.fetchProviders(mockDomain); - - await waitFor(() => { - expect(result.current.isLoadingProviders).toBe(false); - }); - - expect(result.current.providers).toHaveLength(1); - const firstProvider = result.current.providers[0]; - expect(firstProvider).toBeDefined(); - expect(firstProvider!.is_associated).toBe(true); + expect(mockHandleError).toHaveBeenCalledWith(error, { + fallbackMessage: 'domain_table.notifications.domain_verify.error', }); + }); - it('should fetch providers from cache via ensureQueryData', async () => { - const mockDomain = createMockDomain(); - const provider1 = createMockIdentityProvider({ - id: 'provider-1', - display_name: 'Provider 1', - domains: [mockDomain.domain], - }); - - mockCoreClient.getMyOrganizationApiClient().organization.identityProviders.list = vi - .fn() - .mockResolvedValue({ - identity_providers: [provider1], - }); - - const { result } = renderUseDomainTable(mockOptions); - - // First fetch - await result.current.fetchProviders(mockDomain); + it('should delete domain and close modals on handleDelete', async () => { + const mockOnDeleteDomain = vi.fn().mockResolvedValue(undefined); + mockUseDomainTableService.mockReturnValue( + createMockDomainTableServiceReturn({ onDeleteDomain: mockOnDeleteDomain }), + ); - await waitFor(() => { - expect(result.current.isLoadingProviders).toBe(false); - }); + const { result } = renderHook(() => useDomainTable(defaultOptions)); - const initialApiCallCount = vi.mocked( - mockCoreClient.getMyOrganizationApiClient().organization.identityProviders.list, - ).mock.calls.length; + await act(async () => { + await result.current.handleDelete(mockDomain); + }); - // Second fetch - should use cached data since it's fresh - await result.current.fetchProviders(mockDomain); + expect(mockOnDeleteDomain).toHaveBeenCalledWith(mockDomain); + expect(result.current.showDeleteModal).toBe(false); + expect(result.current.showVerifyModal).toBe(false); + }); - await waitFor(() => { - expect(result.current.isLoadingProviders).toBe(false); - }); + it('should handle delete error on handleDelete', async () => { + const error = new Error('Delete failed'); + const mockOnDeleteDomain = vi.fn().mockRejectedValue(error); + mockUseDomainTableService.mockReturnValue( + createMockDomainTableServiceReturn({ onDeleteDomain: mockOnDeleteDomain }), + ); - // Verify providers are loaded correctly - expect(result.current.providers).toHaveLength(1); - const cachedProvider = result.current.providers[0]; - expect(cachedProvider).toBeDefined(); - expect(cachedProvider!.is_associated).toBe(true); + const { result } = renderHook(() => useDomainTable(defaultOptions)); - // Should use cache if available and fresh (not make additional API calls) - const finalApiCallCount = vi.mocked( - mockCoreClient.getMyOrganizationApiClient().organization.identityProviders.list, - ).mock.calls.length; + await act(async () => { + await result.current.handleDelete(mockDomain); + }); - expect(finalApiCallCount).toBe(initialApiCallCount); + expect(mockHandleError).toHaveBeenCalledWith(error, { + fallbackMessage: 'domain_table.notifications.domain_delete.error', }); }); - describe('onCreateDomain', () => { - it('should create domain successfully with callbacks', async () => { - const mockDomain = createMockDomain(); - const createData: CreateOrganizationDomainRequestContent = { domain: mockDomain.domain }; - - const { result } = renderUseDomainTable(mockOptions); + it('should associate domain to provider on handleToggleSwitch with true', async () => { + const mockOnAssociateToProvider = vi.fn().mockResolvedValue(undefined); + mockUseDomainTableService.mockReturnValue( + createMockDomainTableServiceReturn({ onAssociateToProvider: mockOnAssociateToProvider }), + ); - await result.current.onCreateDomain(createData); + const { result } = renderHook(() => useDomainTable(defaultOptions)); - await waitFor(() => { - expect(result.current.isCreating).toBe(false); - }); - - expect(mockOptions.createAction!.onBefore).toHaveBeenCalledWith(createData); - expect( - mockCoreClient.getMyOrganizationApiClient().organization.domains.create, - ).toHaveBeenCalledWith(createData); + await act(async () => { + await result.current.handleToggleSwitch(mockDomain, mockProvider, true); }); - it('should handle onBefore callback returning false', async () => { - const createData: CreateOrganizationDomainRequestContent = { domain: 'test.com' }; - const mockOptionsWithFalseBefore = createMockOptions({ - createAction: { - onBefore: vi.fn().mockReturnValue(false), - onAfter: vi.fn(), - }, - }); + expect(mockOnAssociateToProvider).toHaveBeenCalledWith(mockDomain, mockProvider); + }); - const { result } = renderUseDomainTable(mockOptionsWithFalseBefore); + it('should delete domain from provider on handleToggleSwitch with false', async () => { + const mockOnDeleteFromProvider = vi.fn().mockResolvedValue(undefined); + mockUseDomainTableService.mockReturnValue( + createMockDomainTableServiceReturn({ onDeleteFromProvider: mockOnDeleteFromProvider }), + ); - await expect(result.current.onCreateDomain(createData)).rejects.toThrow(BusinessError); + const { result } = renderHook(() => useDomainTable(defaultOptions)); - expect( - mockCoreClient.getMyOrganizationApiClient().organization.domains.create, - ).not.toHaveBeenCalled(); + await act(async () => { + await result.current.handleToggleSwitch(mockDomain, mockProvider, false); }); - it('should handle create domain API error', async () => { - const createData: CreateOrganizationDomainRequestContent = { domain: 'test.com' }; - const error = new Error('API error'); - mockCoreClient.getMyOrganizationApiClient().organization.domains.create = vi - .fn() - .mockRejectedValue(error); - - const { result } = renderUseDomainTable(mockOptions); + expect(mockOnDeleteFromProvider).toHaveBeenCalledWith(mockDomain, mockProvider); + }); - await expect(result.current.onCreateDomain(createData)).rejects.toThrow('API error'); + it('should handle associate to provider error on handleToggleSwitch', async () => { + const error = new Error('Associate failed'); + const mockOnAssociateToProvider = vi.fn().mockRejectedValue(error); + mockUseDomainTableService.mockReturnValue( + createMockDomainTableServiceReturn({ onAssociateToProvider: mockOnAssociateToProvider }), + ); - await waitFor(() => { - expect(result.current.isCreating).toBe(false); - }); + const { result } = renderHook(() => useDomainTable(defaultOptions)); - expect(result.current.isCreating).toBe(false); + await act(async () => { + await result.current.handleToggleSwitch(mockDomain, mockProvider, true); }); - it('should work without onBefore and onAfter callbacks', async () => { - const mockDomain = createMockDomain(); - const createData: CreateOrganizationDomainRequestContent = { domain: mockDomain.domain }; - const mockOptionsWithoutCallbacks = createMockOptions({ - createAction: undefined, - }); - - const { result } = renderUseDomainTable(mockOptionsWithoutCallbacks); - - await result.current.onCreateDomain(createData); - - await waitFor(() => { - expect(result.current.isCreating).toBe(false); - }); - - expect( - mockCoreClient.getMyOrganizationApiClient().organization.domains.create, - ).toHaveBeenCalledWith(createData); + expect(mockHandleError).toHaveBeenCalledWith(error, { + fallbackMessage: 'domain_table.notifications.domain_associate_provider.error', }); }); - describe('onVerifyDomain', () => { - it('should verify domain successfully and return true', async () => { - const mockDomain = createMockDomain(); - const { result } = renderUseDomainTable(mockOptions); - - const isVerified = await result.current.onVerifyDomain(mockDomain); + it('should handle delete from provider error on handleToggleSwitch', async () => { + const error = new Error('Delete from provider failed'); + const mockOnDeleteFromProvider = vi.fn().mockRejectedValue(error); + mockUseDomainTableService.mockReturnValue( + createMockDomainTableServiceReturn({ onDeleteFromProvider: mockOnDeleteFromProvider }), + ); - await waitFor(() => { - expect(result.current.isVerifying).toBe(false); - }); + const { result } = renderHook(() => useDomainTable(defaultOptions)); - expect(mockOptions.verifyAction!.onBefore).toHaveBeenCalledWith(mockDomain); - expect( - mockCoreClient.getMyOrganizationApiClient().organization.domains.verify.create, - ).toHaveBeenCalledWith(mockDomain.id); - expect(isVerified).toBe(true); + await act(async () => { + await result.current.handleToggleSwitch(mockDomain, mockProvider, false); }); - it('should verify domain and return false when status is not verified', async () => { - const mockDomain = createMockDomain(); - mockCoreClient.getMyOrganizationApiClient().organization.domains.verify.create = vi - .fn() - .mockResolvedValue({ - status: 'pending', - }); - - const { result } = renderUseDomainTable(mockOptions); - - const isVerified = await result.current.onVerifyDomain(mockDomain); - - await waitFor(() => { - expect(result.current.isVerifying).toBe(false); - }); - - expect(isVerified).toBe(false); + expect(mockHandleError).toHaveBeenCalledWith(error, { + fallbackMessage: 'domain_table.notifications.domain_delete_provider.error', }); + }); - it('should handle onBefore callback returning false', async () => { - const mockDomain = createMockDomain(); - const mockOptionsWithFalseBefore = createMockOptions({ - verifyAction: { - onBefore: vi.fn().mockReturnValue(false), - onAfter: vi.fn(), - }, - }); - - const { result } = renderUseDomainTable(mockOptionsWithFalseBefore); + it('should close verify modal and clear error on handleCloseVerifyModal', async () => { + const mockOnVerifyDomain = vi.fn().mockResolvedValue(false); + mockUseDomainTableService.mockReturnValue( + createMockDomainTableServiceReturn({ onVerifyDomain: mockOnVerifyDomain }), + ); - await expect(result.current.onVerifyDomain(mockDomain)).rejects.toThrow(BusinessError); + const { result } = renderHook(() => useDomainTable(defaultOptions)); - expect( - mockCoreClient.getMyOrganizationApiClient().organization.domains.verify.create, - ).not.toHaveBeenCalled(); + await act(async () => { + await result.current.handleVerify(mockDomain); }); + expect(result.current.verifyError).toBeDefined(); - it('should work without onBefore and onAfter callbacks', async () => { - const mockDomain = createMockDomain(); - const mockOptionsWithoutCallbacks = createMockOptions({ - verifyAction: undefined, - }); - - const { result } = renderUseDomainTable(mockOptionsWithoutCallbacks); - - const isVerified = await result.current.onVerifyDomain(mockDomain); - - await waitFor(() => { - expect(result.current.isVerifying).toBe(false); - }); - - expect( - mockCoreClient.getMyOrganizationApiClient().organization.domains.verify.create, - ).toHaveBeenCalledWith(mockDomain.id); - expect(isVerified).toBe(true); + act(() => { + result.current.handleCloseVerifyModal(); }); - }); - - describe('onDeleteDomain', () => { - it('should delete domain successfully with callbacks', async () => { - const mockDomain = createMockDomain(); - const { result } = renderUseDomainTable(mockOptions); - await result.current.onDeleteDomain(mockDomain); + expect(result.current.showVerifyModal).toBe(false); + expect(result.current.verifyError).toBeUndefined(); + }); - await waitFor(() => { - expect(result.current.isDeleting).toBe(false); - }); + it('should show verify modal for unverified domain on handleConfigureClick', async () => { + const { result } = renderHook(() => useDomainTable(defaultOptions)); - expect(mockOptions.deleteAction!.onBefore).toHaveBeenCalledWith(mockDomain); - expect( - mockCoreClient.getMyOrganizationApiClient().organization.domains.delete, - ).toHaveBeenCalledWith(mockDomain.id); - expect(mockOptions.deleteAction!.onAfter).toHaveBeenCalledWith(mockDomain); + await act(async () => { + await result.current.handleConfigureClick(mockDomain); }); - it('should handle onBefore callback returning false', async () => { - const mockDomain = createMockDomain(); - const mockOptionsWithFalseBefore = createMockOptions({ - deleteAction: { - onBefore: vi.fn().mockReturnValue(false), - onAfter: vi.fn(), - }, - }); + expect(result.current.selectedDomain).toEqual(mockDomain); + expect(result.current.showVerifyModal).toBe(true); + }); - const { result } = renderUseDomainTable(mockOptionsWithFalseBefore); + it('should fetch providers and show configure modal for verified domain on handleConfigureClick', async () => { + const mockFetchProviders = vi.fn().mockResolvedValue(undefined); + mockUseDomainTableService.mockReturnValue( + createMockDomainTableServiceReturn({ fetchProviders: mockFetchProviders }), + ); - await expect(result.current.onDeleteDomain(mockDomain)).rejects.toThrow(BusinessError); + const { result } = renderHook(() => useDomainTable(defaultOptions)); - expect( - mockCoreClient.getMyOrganizationApiClient().organization.domains.delete, - ).not.toHaveBeenCalled(); + await act(async () => { + await result.current.handleConfigureClick(verifiedDomain); }); - it('should work without onBefore and onAfter callbacks', async () => { - const mockDomain = createMockDomain(); - const mockOptionsWithoutCallbacks = createMockOptions({ - deleteAction: undefined, - }); - - const { result } = renderUseDomainTable(mockOptionsWithoutCallbacks); - - await result.current.onDeleteDomain(mockDomain); - - await waitFor(() => { - expect(result.current.isDeleting).toBe(false); - }); - - expect( - mockCoreClient.getMyOrganizationApiClient().organization.domains.delete, - ).toHaveBeenCalledWith(mockDomain.id); - }); + expect(result.current.selectedDomain).toEqual(verifiedDomain); + expect(mockFetchProviders).toHaveBeenCalledWith(verifiedDomain); + expect(result.current.showConfigureModal).toBe(true); }); - describe('onAssociateToProvider', () => { - it('should associate domain to provider successfully', async () => { - const mockDomain = createMockDomain(); - const mockProvider = createMockIdentityProvider(); + it('should handle fetchProviders error on handleConfigureClick', async () => { + const error = new Error('Fetch providers failed'); + const mockFetchProviders = vi.fn().mockRejectedValue(error); + mockUseDomainTableService.mockReturnValue( + createMockDomainTableServiceReturn({ fetchProviders: mockFetchProviders }), + ); - const { result } = renderUseDomainTable(mockOptions); + const { result } = renderHook(() => useDomainTable(defaultOptions)); - await result.current.onAssociateToProvider(mockDomain, mockProvider); - - await waitFor(() => { - expect(result.current.isCreating).toBe(false); - }); - - expect(mockOptions.associateToProviderAction!.onBefore).toHaveBeenCalledWith( - mockDomain, - mockProvider, - ); - expect( - mockCoreClient.getMyOrganizationApiClient().organization.identityProviders.domains.create, - ).toHaveBeenCalledWith(mockProvider.id, { domain: mockDomain.domain }); + await act(async () => { + await result.current.handleConfigureClick(verifiedDomain); }); - it('should handle onBefore callback returning false', async () => { - const mockDomain = createMockDomain(); - const mockProvider = createMockIdentityProvider(); - const mockOptionsWithFalseBefore = createMockOptions({ - associateToProviderAction: { - onBefore: vi.fn().mockReturnValue(false), - onAfter: vi.fn(), - }, - }); - - const { result } = renderUseDomainTable(mockOptionsWithFalseBefore); - - await expect(result.current.onAssociateToProvider(mockDomain, mockProvider)).rejects.toThrow( - BusinessError, - ); - - expect( - mockCoreClient.getMyOrganizationApiClient().organization.identityProviders.domains.create, - ).not.toHaveBeenCalled(); + expect(mockHandleError).toHaveBeenCalledWith(error, { + fallbackMessage: 'domain_table.notifications.fetch_providers_error', }); + }); - it('should work without onBefore and onAfter callbacks', async () => { - const mockDomain = createMockDomain(); - const mockProvider = createMockIdentityProvider(); - const mockOptionsWithoutCallbacks = createMockOptions({ - associateToProviderAction: undefined, - }); - - const { result } = renderUseDomainTable(mockOptionsWithoutCallbacks); - - await result.current.onAssociateToProvider(mockDomain, mockProvider); + it('should verify, fetch providers, and show configure modal on handleVerifyClick success', async () => { + const mockOnVerifyDomain = vi.fn().mockResolvedValue(true); + const mockFetchProviders = vi.fn().mockResolvedValue(undefined); + mockUseDomainTableService.mockReturnValue( + createMockDomainTableServiceReturn({ + onVerifyDomain: mockOnVerifyDomain, + fetchProviders: mockFetchProviders, + }), + ); - await waitFor(() => { - expect(result.current.isCreating).toBe(false); - }); + const { result } = renderHook(() => useDomainTable(defaultOptions)); - expect( - mockCoreClient.getMyOrganizationApiClient().organization.identityProviders.domains.create, - ).toHaveBeenCalledWith(mockProvider.id, { domain: mockDomain.domain }); + await act(async () => { + await result.current.handleVerifyClick(mockDomain); }); - }); - describe('onDeleteFromProvider', () => { - it('should delete domain from provider successfully', async () => { - const mockDomain = createMockDomain(); - const mockProvider = createMockIdentityProvider(); - - const { result } = renderUseDomainTable(mockOptions); - - await result.current.onDeleteFromProvider(mockDomain, mockProvider); + expect(result.current.selectedDomain).toEqual(mockDomain); + expect(mockOnVerifyDomain).toHaveBeenCalledWith(mockDomain); + expect(mockFetchProviders).toHaveBeenCalledWith(mockDomain); + expect(result.current.showConfigureModal).toBe(true); + }); - await waitFor(() => { - expect(result.current.isDeleting).toBe(false); - }); + it('should show error toast on handleVerifyClick failure', async () => { + const mockOnVerifyDomain = vi.fn().mockResolvedValue(false); + mockUseDomainTableService.mockReturnValue( + createMockDomainTableServiceReturn({ onVerifyDomain: mockOnVerifyDomain }), + ); - expect(mockOptions.deleteFromProviderAction!.onBefore).toHaveBeenCalledWith( - mockDomain, - mockProvider, - ); - expect( - mockCoreClient.getMyOrganizationApiClient().organization.identityProviders.domains.delete, - ).toHaveBeenCalledWith(mockProvider.id, mockDomain.domain); - }); + const { result } = renderHook(() => useDomainTable(defaultOptions)); - it('should handle onBefore callback returning false', async () => { - const mockDomain = createMockDomain(); - const mockProvider = createMockIdentityProvider(); - const mockOptionsWithFalseBefore = createMockOptions({ - deleteFromProviderAction: { - onBefore: vi.fn().mockReturnValue(false), - onAfter: vi.fn(), - }, - }); - - const { result } = renderUseDomainTable(mockOptionsWithFalseBefore); - - await expect(result.current.onDeleteFromProvider(mockDomain, mockProvider)).rejects.toThrow( - BusinessError, - ); - - expect( - mockCoreClient.getMyOrganizationApiClient().organization.identityProviders.domains.delete, - ).not.toHaveBeenCalled(); + await act(async () => { + await result.current.handleVerifyClick(mockDomain); }); - it('should work without onBefore and onAfter callbacks', async () => { - const mockDomain = createMockDomain(); - const mockProvider = createMockIdentityProvider(); - const mockOptionsWithoutCallbacks = createMockOptions({ - deleteFromProviderAction: undefined, - }); + expect(result.current.selectedDomain).toEqual(mockDomain); + }); - const { result } = renderUseDomainTable(mockOptionsWithoutCallbacks); + it('should handle error on handleVerifyClick', async () => { + const error = new Error('Verify click failed'); + const mockOnVerifyDomain = vi.fn().mockRejectedValue(error); + mockUseDomainTableService.mockReturnValue( + createMockDomainTableServiceReturn({ onVerifyDomain: mockOnVerifyDomain }), + ); - await result.current.onDeleteFromProvider(mockDomain, mockProvider); + const { result } = renderHook(() => useDomainTable(defaultOptions)); - await waitFor(() => { - expect(result.current.isDeleting).toBe(false); - }); + await act(async () => { + await result.current.handleVerifyClick(mockDomain); + }); - expect( - mockCoreClient.getMyOrganizationApiClient().organization.identityProviders.domains.delete, - ).toHaveBeenCalledWith(mockProvider.id, mockDomain.domain); + expect(mockHandleError).toHaveBeenCalledWith(error, { + fallbackMessage: 'domain_table.notifications.domain_verify.error', }); }); - describe('Edge Cases and Integration', () => { - it('should handle provider with undefined id in onAssociateToProvider', async () => { - const mockDomain = createMockDomain(); - const mockProvider = createMockIdentityProvider({ id: undefined }); - - const { result } = renderUseDomainTable(mockOptions); - - await result.current.onAssociateToProvider(mockDomain, mockProvider); - - await waitFor(() => { - expect(result.current.isCreating).toBe(false); - }); + it('should set selected domain, close verify modal, and show delete modal on handleDeleteClick', () => { + const { result } = renderHook(() => useDomainTable(defaultOptions)); - expect( - mockCoreClient.getMyOrganizationApiClient().organization.identityProviders.domains.create, - ).toHaveBeenCalledWith(undefined, { domain: mockDomain.domain }); + act(() => { + result.current.setShowVerifyModal(true); }); - it('should handle provider with undefined id in onDeleteFromProvider', async () => { - const mockDomain = createMockDomain(); - const mockProvider = createMockIdentityProvider({ id: undefined }); - - const { result } = renderUseDomainTable(mockOptions); - - await result.current.onDeleteFromProvider(mockDomain, mockProvider); - - await waitFor(() => { - expect(result.current.isDeleting).toBe(false); - }); - - expect( - mockCoreClient.getMyOrganizationApiClient().organization.identityProviders.domains.delete, - ).toHaveBeenCalledWith(undefined, mockDomain.domain); + act(() => { + result.current.handleDeleteClick(mockDomain); }); - it('should call useTranslator with correct parameters', () => { - const useTranslatorSpy = vi.spyOn(useTranslatorModule, 'useTranslator'); - renderUseDomainTable(mockOptions); - - expect(useTranslatorSpy).toHaveBeenCalledWith( - 'domain_management.domain_table.notifications', - {}, - ); - }); + expect(result.current.selectedDomain).toEqual(mockDomain); + expect(result.current.showVerifyModal).toBe(false); + expect(result.current.showDeleteModal).toBe(true); }); }); diff --git a/packages/react/src/hooks/my-organization/shared/__tests__/use-domain-table-service.test.ts b/packages/react/src/hooks/my-organization/shared/__tests__/use-domain-table-service.test.ts new file mode 100644 index 000000000..371f05594 --- /dev/null +++ b/packages/react/src/hooks/my-organization/shared/__tests__/use-domain-table-service.test.ts @@ -0,0 +1,807 @@ +import type { + CreateOrganizationDomainRequestContent, + EnhancedTranslationFunction, +} from '@auth0/universal-components-core'; +import { BusinessError } from '@auth0/universal-components-core'; +import { renderHook, waitFor } from '@testing-library/react'; +import { describe, it, expect, beforeEach, vi } from 'vitest'; + +import { useDomainTableService } from '@/hooks/my-organization/shared/services/use-domain-table-service'; +import * as useCoreClientModule from '@/hooks/shared/use-core-client'; +import * as useTranslatorModule from '@/hooks/shared/use-translator'; +import { + mockCore, + createMockDomain, + createMockIdentityProvider, + createMockI18nService, + createMockDomainTableServiceOptions, +} from '@/tests/utils'; +import { createTestQueryClientWrapper } from '@/tests/utils/test-provider'; +import type { UseDomainTableServiceOptions } from '@/types/my-organization/domain-management/domain-table-types'; + +const { initMockCoreClient } = mockCore(); + +const renderUseDomainTable = (options: UseDomainTableServiceOptions) => { + const { wrapper, queryClient } = createTestQueryClientWrapper(); + return { + queryClient, + ...renderHook(() => useDomainTableService(options), { wrapper }), + }; +}; + +describe('useDomainTableService', () => { + let mockCoreClient: ReturnType; + let mockOptions: UseDomainTableServiceOptions; + let mockT: EnhancedTranslationFunction; + + beforeEach(() => { + vi.clearAllMocks(); + + mockCoreClient = initMockCoreClient(); + mockOptions = createMockDomainTableServiceOptions(); + mockT = createMockI18nService().translator('my-organization'); + + vi.spyOn(useCoreClientModule, 'useCoreClient').mockReturnValue({ + coreClient: mockCoreClient, + }); + + vi.spyOn(useTranslatorModule, 'useTranslator').mockReturnValue({ + t: mockT, + changeLanguage: vi.fn(), + currentLanguage: 'en', + fallbackLanguage: 'en', + }); + }); + + describe('Initial State', () => { + it('should initialize with correct default state', async () => { + const { result } = renderUseDomainTable(mockOptions); + + // Initial state before query completes + expect(result.current.domains).toEqual([]); + expect(result.current.providers).toEqual([]); + expect(result.current.isCreating).toBe(false); + expect(result.current.isDeleting).toBe(false); + expect(result.current.isVerifying).toBe(false); + expect(result.current.isLoadingProviders).toBe(false); + + // Wait for initial query to complete + await waitFor(() => { + expect(result.current.isFetching).toBe(false); + }); + }); + + it('should provide all expected functions', () => { + const { result } = renderUseDomainTable(mockOptions); + + expect(typeof result.current.fetchDomains).toBe('function'); + expect(typeof result.current.fetchProviders).toBe('function'); + expect(typeof result.current.onCreateDomain).toBe('function'); + expect(typeof result.current.onVerifyDomain).toBe('function'); + expect(typeof result.current.onDeleteDomain).toBe('function'); + expect(typeof result.current.onAssociateToProvider).toBe('function'); + expect(typeof result.current.onDeleteFromProvider).toBe('function'); + }); + }); + + describe('fetchDomains', () => { + it('should fetch domains successfully', async () => { + const { result } = renderUseDomainTable(mockOptions); + + await result.current.fetchDomains(); + + await waitFor(() => { + expect(result.current.isFetching).toBe(false); + }); + + expect( + mockCoreClient.getMyOrganizationApiClient().organization.domains.list, + ).toHaveBeenCalled(); + }); + + it('should handle fetchDomains error and reset loading state', async () => { + const error = new Error('Network error'); + mockCoreClient.getMyOrganizationApiClient().organization.domains.list = vi + .fn() + .mockRejectedValue(error); + + const { result } = renderUseDomainTable(mockOptions); + + await result.current.fetchDomains(); + + await waitFor(() => { + expect(result.current.isFetching).toBe(false); + }); + + expect(result.current.isFetching).toBe(false); + }); + + it('should handle empty domains response', async () => { + const { result } = renderUseDomainTable(mockOptions); + + await result.current.fetchDomains(); + + await waitFor(() => { + expect(result.current.isFetching).toBe(false); + }); + + expect(result.current.domains).toEqual([]); + }); + + it('should read from cache without refetching when fetchDomains is called', async () => { + const { result } = renderUseDomainTable(mockOptions); + + // Wait for initial fetch to complete + await waitFor(() => { + expect(result.current.isFetching).toBe(false); + }); + + const initialCallCount = vi.mocked( + mockCoreClient.getMyOrganizationApiClient().organization.domains.list, + ).mock.calls.length; + + // Call fetchDomains - should read from cache without triggering refetch + await result.current.fetchDomains(); + + // Should not trigger additional API calls + expect( + vi.mocked(mockCoreClient.getMyOrganizationApiClient().organization.domains.list).mock.calls + .length, + ).toBe(initialCallCount); + }); + + it('should refetch when data is invalidated', async () => { + const { result, queryClient } = renderUseDomainTable(mockOptions); + + // Wait for initial fetch to complete + await waitFor(() => { + expect(result.current.isFetching).toBe(false); + }); + + const initialCallCount = vi.mocked( + mockCoreClient.getMyOrganizationApiClient().organization.domains.list, + ).mock.calls.length; + + // Invalidate the query + await queryClient.invalidateQueries({ queryKey: ['domains', 'list'] }); + + // Call fetchDomains + await result.current.fetchDomains(); + + // Should call the API again due to invalidation + await waitFor(() => { + expect( + vi.mocked(mockCoreClient.getMyOrganizationApiClient().organization.domains.list).mock + .calls.length, + ).toBeGreaterThan(initialCallCount); + }); + }); + }); + + describe('fetchProviders', () => { + it('should fetch providers with correct association status', async () => { + const mockDomain = createMockDomain(); + const provider1 = createMockIdentityProvider({ + id: 'provider-1', + display_name: 'Provider 1', + domains: [mockDomain.domain], + }); + const provider2 = createMockIdentityProvider({ + id: 'provider-2', + display_name: 'Provider 2', + domains: [], + }); + const provider3 = createMockIdentityProvider({ + id: 'provider-3', + display_name: 'Provider 3', + domains: [mockDomain.domain], + }); + + // Mock all providers response - domains field indicates association + mockCoreClient.getMyOrganizationApiClient().organization.identityProviders.list = vi + .fn() + .mockResolvedValue({ + identity_providers: [provider1, provider2, provider3], + }); + + const { result } = renderUseDomainTable(mockOptions); + + await result.current.fetchProviders(mockDomain); + + await waitFor(() => { + expect(result.current.isLoadingProviders).toBe(false); + }); + + expect( + mockCoreClient.getMyOrganizationApiClient().organization.identityProviders.list, + ).toHaveBeenCalled(); + + // Verify the providers are correctly matched with association status + expect(result.current.providers).toHaveLength(3); + + // Provider 1 should be associated + const resultProvider1 = result.current.providers.find((p) => p.id === 'provider-1'); + expect(resultProvider1).toBeDefined(); + expect(resultProvider1!.is_associated).toBe(true); + expect(resultProvider1!.display_name).toBe('Provider 1'); + + // Provider 2 should NOT be associated + const resultProvider2 = result.current.providers.find((p) => p.id === 'provider-2'); + expect(resultProvider2).toBeDefined(); + expect(resultProvider2!.is_associated).toBe(false); + expect(resultProvider2!.display_name).toBe('Provider 2'); + + // Provider 3 should be associated + const resultProvider3 = result.current.providers.find((p) => p.id === 'provider-3'); + expect(resultProvider3).toBeDefined(); + expect(resultProvider3!.is_associated).toBe(true); + expect(resultProvider3!.display_name).toBe('Provider 3'); + }); + + it('should handle providers with no associations', async () => { + const mockDomain = createMockDomain(); + const provider1 = createMockIdentityProvider({ + id: 'provider-1', + display_name: 'Provider 1', + domains: [], + }); + const provider2 = createMockIdentityProvider({ + id: 'provider-2', + display_name: 'Provider 2', + domains: [], + }); + + // Mock all providers response - no domains associated + mockCoreClient.getMyOrganizationApiClient().organization.identityProviders.list = vi + .fn() + .mockResolvedValue({ + identity_providers: [provider1, provider2], + }); + + const { result } = renderUseDomainTable(mockOptions); + + await result.current.fetchProviders(mockDomain); + + await waitFor(() => { + expect(result.current.isLoadingProviders).toBe(false); + }); + + // All providers should have is_associated = false + expect(result.current.providers).toHaveLength(2); + result.current.providers.forEach((provider) => { + expect(provider.is_associated).toBe(false); + }); + }); + + it('should handle all providers being associated', async () => { + const mockDomain = createMockDomain(); + const provider1 = createMockIdentityProvider({ + id: 'provider-1', + display_name: 'Provider 1', + domains: [mockDomain.domain], + }); + const provider2 = createMockIdentityProvider({ + id: 'provider-2', + display_name: 'Provider 2', + domains: [mockDomain.domain], + }); + + // Mock all providers response - all associated via domains field + mockCoreClient.getMyOrganizationApiClient().organization.identityProviders.list = vi + .fn() + .mockResolvedValue({ + identity_providers: [provider1, provider2], + }); + + const { result } = renderUseDomainTable(mockOptions); + + await result.current.fetchProviders(mockDomain); + + await waitFor(() => { + expect(result.current.isLoadingProviders).toBe(false); + }); + + // All providers should have is_associated = true + expect(result.current.providers).toHaveLength(2); + result.current.providers.forEach((provider) => { + expect(provider.is_associated).toBe(true); + }); + }); + + it('should handle fetchProviders error and reset loading state', async () => { + const mockDomain = createMockDomain(); + const error = new Error('Network error'); + mockCoreClient.getMyOrganizationApiClient().organization.identityProviders.list = vi + .fn() + .mockRejectedValue(error); + + const { result } = renderUseDomainTable(mockOptions); + + await expect(result.current.fetchProviders(mockDomain)).rejects.toThrow('Network error'); + + await waitFor(() => { + expect(result.current.isLoadingProviders).toBe(false); + }); + }); + + it('should handle null/undefined responses gracefully', async () => { + const mockDomain = createMockDomain(); + + // Mock null response + mockCoreClient.getMyOrganizationApiClient().organization.identityProviders.list = vi + .fn() + .mockResolvedValue({ + identity_providers: null, + }); + + const { result } = renderUseDomainTable(mockOptions); + + await result.current.fetchProviders(mockDomain); + + await waitFor(() => { + expect(result.current.isLoadingProviders).toBe(false); + }); + + // Should handle null gracefully and return empty array + expect(result.current.providers).toEqual([]); + }); + + it('should use ensureQueryData to fetch providers', async () => { + const mockDomain = createMockDomain(); + const provider1 = createMockIdentityProvider({ + id: 'provider-1', + display_name: 'Provider 1', + domains: [mockDomain.domain], + }); + + mockCoreClient.getMyOrganizationApiClient().organization.identityProviders.list = vi + .fn() + .mockResolvedValue({ + identity_providers: [provider1], + }); + + const { result } = renderUseDomainTable(mockOptions); + + await result.current.fetchProviders(mockDomain); + + await waitFor(() => { + expect(result.current.isLoadingProviders).toBe(false); + }); + + expect(result.current.providers).toHaveLength(1); + const firstProvider = result.current.providers[0]; + expect(firstProvider).toBeDefined(); + expect(firstProvider!.is_associated).toBe(true); + }); + + it('should fetch providers from cache via ensureQueryData', async () => { + const mockDomain = createMockDomain(); + const provider1 = createMockIdentityProvider({ + id: 'provider-1', + display_name: 'Provider 1', + domains: [mockDomain.domain], + }); + + mockCoreClient.getMyOrganizationApiClient().organization.identityProviders.list = vi + .fn() + .mockResolvedValue({ + identity_providers: [provider1], + }); + + const { result } = renderUseDomainTable(mockOptions); + + // First fetch + await result.current.fetchProviders(mockDomain); + + await waitFor(() => { + expect(result.current.isLoadingProviders).toBe(false); + }); + + const initialApiCallCount = vi.mocked( + mockCoreClient.getMyOrganizationApiClient().organization.identityProviders.list, + ).mock.calls.length; + + // Second fetch - should use cached data since it's fresh + await result.current.fetchProviders(mockDomain); + + await waitFor(() => { + expect(result.current.isLoadingProviders).toBe(false); + }); + + // Verify providers are loaded correctly + expect(result.current.providers).toHaveLength(1); + const cachedProvider = result.current.providers[0]; + expect(cachedProvider).toBeDefined(); + expect(cachedProvider!.is_associated).toBe(true); + + // Should use cache if available and fresh (not make additional API calls) + const finalApiCallCount = vi.mocked( + mockCoreClient.getMyOrganizationApiClient().organization.identityProviders.list, + ).mock.calls.length; + + expect(finalApiCallCount).toBe(initialApiCallCount); + }); + }); + + describe('onCreateDomain', () => { + it('should create domain successfully with callbacks', async () => { + const mockDomain = createMockDomain(); + const createData: CreateOrganizationDomainRequestContent = { domain: mockDomain.domain }; + + const { result } = renderUseDomainTable(mockOptions); + + await result.current.onCreateDomain(createData); + + await waitFor(() => { + expect(result.current.isCreating).toBe(false); + }); + + expect(mockOptions.createAction!.onBefore).toHaveBeenCalledWith(createData); + expect( + mockCoreClient.getMyOrganizationApiClient().organization.domains.create, + ).toHaveBeenCalledWith(createData); + }); + + it('should handle onBefore callback returning false', async () => { + const createData: CreateOrganizationDomainRequestContent = { domain: 'test.com' }; + const mockOptionsWithFalseBefore = createMockDomainTableServiceOptions({ + createAction: { + onBefore: vi.fn().mockReturnValue(false), + onAfter: vi.fn(), + }, + }); + + const { result } = renderUseDomainTable(mockOptionsWithFalseBefore); + + await expect(result.current.onCreateDomain(createData)).rejects.toThrow(BusinessError); + + expect( + mockCoreClient.getMyOrganizationApiClient().organization.domains.create, + ).not.toHaveBeenCalled(); + }); + + it('should handle create domain API error', async () => { + const createData: CreateOrganizationDomainRequestContent = { domain: 'test.com' }; + const error = new Error('API error'); + mockCoreClient.getMyOrganizationApiClient().organization.domains.create = vi + .fn() + .mockRejectedValue(error); + + const { result } = renderUseDomainTable(mockOptions); + + await expect(result.current.onCreateDomain(createData)).rejects.toThrow('API error'); + + await waitFor(() => { + expect(result.current.isCreating).toBe(false); + }); + + expect(result.current.isCreating).toBe(false); + }); + + it('should work without onBefore and onAfter callbacks', async () => { + const mockDomain = createMockDomain(); + const createData: CreateOrganizationDomainRequestContent = { domain: mockDomain.domain }; + const mockOptionsWithoutCallbacks = createMockDomainTableServiceOptions({ + createAction: undefined, + }); + + const { result } = renderUseDomainTable(mockOptionsWithoutCallbacks); + + await result.current.onCreateDomain(createData); + + await waitFor(() => { + expect(result.current.isCreating).toBe(false); + }); + + expect( + mockCoreClient.getMyOrganizationApiClient().organization.domains.create, + ).toHaveBeenCalledWith(createData); + }); + }); + + describe('onVerifyDomain', () => { + it('should verify domain successfully and return true', async () => { + const mockDomain = createMockDomain(); + const { result } = renderUseDomainTable(mockOptions); + + const isVerified = await result.current.onVerifyDomain(mockDomain); + + await waitFor(() => { + expect(result.current.isVerifying).toBe(false); + }); + + expect(mockOptions.verifyAction!.onBefore).toHaveBeenCalledWith(mockDomain); + expect( + mockCoreClient.getMyOrganizationApiClient().organization.domains.verify.create, + ).toHaveBeenCalledWith(mockDomain.id); + expect(isVerified).toBe(true); + }); + + it('should verify domain and return false when status is not verified', async () => { + const mockDomain = createMockDomain(); + mockCoreClient.getMyOrganizationApiClient().organization.domains.verify.create = vi + .fn() + .mockResolvedValue({ + status: 'pending', + }); + + const { result } = renderUseDomainTable(mockOptions); + + const isVerified = await result.current.onVerifyDomain(mockDomain); + + await waitFor(() => { + expect(result.current.isVerifying).toBe(false); + }); + + expect(isVerified).toBe(false); + }); + + it('should handle onBefore callback returning false', async () => { + const mockDomain = createMockDomain(); + const mockOptionsWithFalseBefore = createMockDomainTableServiceOptions({ + verifyAction: { + onBefore: vi.fn().mockReturnValue(false), + onAfter: vi.fn(), + }, + }); + + const { result } = renderUseDomainTable(mockOptionsWithFalseBefore); + + await expect(result.current.onVerifyDomain(mockDomain)).rejects.toThrow(BusinessError); + + expect( + mockCoreClient.getMyOrganizationApiClient().organization.domains.verify.create, + ).not.toHaveBeenCalled(); + }); + + it('should work without onBefore and onAfter callbacks', async () => { + const mockDomain = createMockDomain(); + const mockOptionsWithoutCallbacks = createMockDomainTableServiceOptions({ + verifyAction: undefined, + }); + + const { result } = renderUseDomainTable(mockOptionsWithoutCallbacks); + + const isVerified = await result.current.onVerifyDomain(mockDomain); + + await waitFor(() => { + expect(result.current.isVerifying).toBe(false); + }); + + expect( + mockCoreClient.getMyOrganizationApiClient().organization.domains.verify.create, + ).toHaveBeenCalledWith(mockDomain.id); + expect(isVerified).toBe(true); + }); + }); + + describe('onDeleteDomain', () => { + it('should delete domain successfully with callbacks', async () => { + const mockDomain = createMockDomain(); + const { result } = renderUseDomainTable(mockOptions); + + await result.current.onDeleteDomain(mockDomain); + + await waitFor(() => { + expect(result.current.isDeleting).toBe(false); + }); + + expect(mockOptions.deleteAction!.onBefore).toHaveBeenCalledWith(mockDomain); + expect( + mockCoreClient.getMyOrganizationApiClient().organization.domains.delete, + ).toHaveBeenCalledWith(mockDomain.id); + expect(mockOptions.deleteAction!.onAfter).toHaveBeenCalledWith(mockDomain); + }); + + it('should handle onBefore callback returning false', async () => { + const mockDomain = createMockDomain(); + const mockOptionsWithFalseBefore = createMockDomainTableServiceOptions({ + deleteAction: { + onBefore: vi.fn().mockReturnValue(false), + onAfter: vi.fn(), + }, + }); + + const { result } = renderUseDomainTable(mockOptionsWithFalseBefore); + + await expect(result.current.onDeleteDomain(mockDomain)).rejects.toThrow(BusinessError); + + expect( + mockCoreClient.getMyOrganizationApiClient().organization.domains.delete, + ).not.toHaveBeenCalled(); + }); + + it('should work without onBefore and onAfter callbacks', async () => { + const mockDomain = createMockDomain(); + const mockOptionsWithoutCallbacks = createMockDomainTableServiceOptions({ + deleteAction: undefined, + }); + + const { result } = renderUseDomainTable(mockOptionsWithoutCallbacks); + + await result.current.onDeleteDomain(mockDomain); + + await waitFor(() => { + expect(result.current.isDeleting).toBe(false); + }); + + expect( + mockCoreClient.getMyOrganizationApiClient().organization.domains.delete, + ).toHaveBeenCalledWith(mockDomain.id); + }); + }); + + describe('onAssociateToProvider', () => { + it('should associate domain to provider successfully', async () => { + const mockDomain = createMockDomain(); + const mockProvider = createMockIdentityProvider(); + + const { result } = renderUseDomainTable(mockOptions); + + await result.current.onAssociateToProvider(mockDomain, mockProvider); + + await waitFor(() => { + expect(result.current.isCreating).toBe(false); + }); + + expect(mockOptions.associateToProviderAction!.onBefore).toHaveBeenCalledWith( + mockDomain, + mockProvider, + ); + expect( + mockCoreClient.getMyOrganizationApiClient().organization.identityProviders.domains.create, + ).toHaveBeenCalledWith(mockProvider.id, { domain: mockDomain.domain }); + }); + + it('should handle onBefore callback returning false', async () => { + const mockDomain = createMockDomain(); + const mockProvider = createMockIdentityProvider(); + const mockOptionsWithFalseBefore = createMockDomainTableServiceOptions({ + associateToProviderAction: { + onBefore: vi.fn().mockReturnValue(false), + onAfter: vi.fn(), + }, + }); + + const { result } = renderUseDomainTable(mockOptionsWithFalseBefore); + + await expect(result.current.onAssociateToProvider(mockDomain, mockProvider)).rejects.toThrow( + BusinessError, + ); + + expect( + mockCoreClient.getMyOrganizationApiClient().organization.identityProviders.domains.create, + ).not.toHaveBeenCalled(); + }); + + it('should work without onBefore and onAfter callbacks', async () => { + const mockDomain = createMockDomain(); + const mockProvider = createMockIdentityProvider(); + const mockOptionsWithoutCallbacks = createMockDomainTableServiceOptions({ + associateToProviderAction: undefined, + }); + + const { result } = renderUseDomainTable(mockOptionsWithoutCallbacks); + + await result.current.onAssociateToProvider(mockDomain, mockProvider); + + await waitFor(() => { + expect(result.current.isCreating).toBe(false); + }); + + expect( + mockCoreClient.getMyOrganizationApiClient().organization.identityProviders.domains.create, + ).toHaveBeenCalledWith(mockProvider.id, { domain: mockDomain.domain }); + }); + }); + + describe('onDeleteFromProvider', () => { + it('should delete domain from provider successfully', async () => { + const mockDomain = createMockDomain(); + const mockProvider = createMockIdentityProvider(); + + const { result } = renderUseDomainTable(mockOptions); + + await result.current.onDeleteFromProvider(mockDomain, mockProvider); + + await waitFor(() => { + expect(result.current.isDeleting).toBe(false); + }); + + expect(mockOptions.deleteFromProviderAction!.onBefore).toHaveBeenCalledWith( + mockDomain, + mockProvider, + ); + expect( + mockCoreClient.getMyOrganizationApiClient().organization.identityProviders.domains.delete, + ).toHaveBeenCalledWith(mockProvider.id, mockDomain.domain); + }); + + it('should handle onBefore callback returning false', async () => { + const mockDomain = createMockDomain(); + const mockProvider = createMockIdentityProvider(); + const mockOptionsWithFalseBefore = createMockDomainTableServiceOptions({ + deleteFromProviderAction: { + onBefore: vi.fn().mockReturnValue(false), + onAfter: vi.fn(), + }, + }); + + const { result } = renderUseDomainTable(mockOptionsWithFalseBefore); + + await expect(result.current.onDeleteFromProvider(mockDomain, mockProvider)).rejects.toThrow( + BusinessError, + ); + + expect( + mockCoreClient.getMyOrganizationApiClient().organization.identityProviders.domains.delete, + ).not.toHaveBeenCalled(); + }); + + it('should work without onBefore and onAfter callbacks', async () => { + const mockDomain = createMockDomain(); + const mockProvider = createMockIdentityProvider(); + const mockOptionsWithoutCallbacks = createMockDomainTableServiceOptions({ + deleteFromProviderAction: undefined, + }); + + const { result } = renderUseDomainTable(mockOptionsWithoutCallbacks); + + await result.current.onDeleteFromProvider(mockDomain, mockProvider); + + await waitFor(() => { + expect(result.current.isDeleting).toBe(false); + }); + + expect( + mockCoreClient.getMyOrganizationApiClient().organization.identityProviders.domains.delete, + ).toHaveBeenCalledWith(mockProvider.id, mockDomain.domain); + }); + }); + + describe('Edge Cases and Integration', () => { + it('should handle provider with undefined id in onAssociateToProvider', async () => { + const mockDomain = createMockDomain(); + const mockProvider = createMockIdentityProvider({ id: undefined }); + + const { result } = renderUseDomainTable(mockOptions); + + await result.current.onAssociateToProvider(mockDomain, mockProvider); + + await waitFor(() => { + expect(result.current.isCreating).toBe(false); + }); + + expect( + mockCoreClient.getMyOrganizationApiClient().organization.identityProviders.domains.create, + ).toHaveBeenCalledWith(undefined, { domain: mockDomain.domain }); + }); + + it('should handle provider with undefined id in onDeleteFromProvider', async () => { + const mockDomain = createMockDomain(); + const mockProvider = createMockIdentityProvider({ id: undefined }); + + const { result } = renderUseDomainTable(mockOptions); + + await result.current.onDeleteFromProvider(mockDomain, mockProvider); + + await waitFor(() => { + expect(result.current.isDeleting).toBe(false); + }); + + expect( + mockCoreClient.getMyOrganizationApiClient().organization.identityProviders.domains.delete, + ).toHaveBeenCalledWith(undefined, mockDomain.domain); + }); + + it('should call useTranslator with correct parameters', () => { + const useTranslatorSpy = vi.spyOn(useTranslatorModule, 'useTranslator'); + renderUseDomainTable(mockOptions); + + expect(useTranslatorSpy).toHaveBeenCalledWith( + 'domain_management.domain_table.notifications', + {}, + ); + }); + }); +}); diff --git a/packages/react/src/hooks/my-organization/shared/services/use-domain-table-service.ts b/packages/react/src/hooks/my-organization/shared/services/use-domain-table-service.ts new file mode 100644 index 000000000..6c3023f65 --- /dev/null +++ b/packages/react/src/hooks/my-organization/shared/services/use-domain-table-service.ts @@ -0,0 +1,222 @@ +/** + * Internal domain table service hook. + * Handles data fetching and CRUD operations for domains. + * @module use-domain-table-service + * @internal + */ + +import { + type Domain, + type IdpKnownResponse, + type CreateOrganizationDomainRequestContent, + type IdentityProviderAssociatedWithDomain, + BusinessError, +} from '@auth0/universal-components-core'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { useCallback, useState } from 'react'; + +import { useCoreClient } from '@/hooks/shared/use-core-client'; +import { useTranslator } from '@/hooks/shared/use-translator'; +import type { + UseDomainTableServiceOptions, + UseDomainTableServiceReturn, +} from '@/types/my-organization/domain-management/domain-table-types'; + +const domainQueryKeys = { + all: ['domains'] as const, + list: () => [...domainQueryKeys.all, 'list'] as const, + providers: (domainId: string) => [...domainQueryKeys.all, 'providers', domainId] as const, +}; + +/** + * Internal service hook for domain table data and CRUD operations. + * @param options - Service options including actions and custom messages. + * @returns Domain data, mutations, and actions. + * @internal + */ +export function useDomainTableService({ + createAction, + deleteAction, + verifyAction, + associateToProviderAction, + deleteFromProviderAction, + customMessages, +}: UseDomainTableServiceOptions): UseDomainTableServiceReturn { + const { t } = useTranslator('domain_management.domain_table.notifications', customMessages); + const { coreClient } = useCoreClient(); + const queryClient = useQueryClient(); + + const [selectedDomainId, setSelectedDomainId] = useState(null); + const [selectedDomainName, setSelectedDomainName] = useState(null); + + const fetchProvidersForDomain = async (domainName: string) => { + const api = coreClient!.getMyOrganizationApiClient(); + + const allProvidersResponse = await api.organization.identityProviders.list(); + const allProviders = allProvidersResponse?.identity_providers ?? []; + + return allProviders.map( + (provider): IdentityProviderAssociatedWithDomain => ({ + ...provider, + is_associated: provider.domains?.includes(domainName) ?? false, + }), + ); + }; + + const domainsQuery = useQuery({ + queryKey: domainQueryKeys.list(), + queryFn: async () => { + const { response } = await coreClient! + .getMyOrganizationApiClient() + .organization.domains.list(); + return response?.organization_domains ?? []; + }, + enabled: !!coreClient, + }); + + const providersQuery = useQuery({ + queryKey: domainQueryKeys.providers(selectedDomainId ?? ''), + queryFn: () => fetchProvidersForDomain(selectedDomainName!), + enabled: !!coreClient && !!selectedDomainId && !!selectedDomainName, + }); + + const createDomainMutation = useMutation({ + mutationFn: async (data: CreateOrganizationDomainRequestContent): Promise => { + if (createAction?.onBefore && !createAction.onBefore(data as Domain)) { + throw new BusinessError({ message: t('domain_create.on_before') }); + } + return coreClient!.getMyOrganizationApiClient().organization.domains.create(data); + }, + onSuccess: (result) => { + createAction?.onAfter?.(result); + queryClient.invalidateQueries({ queryKey: domainQueryKeys.list() }); + }, + }); + + const verifyDomainMutation = useMutation({ + mutationFn: async (domain: Domain): Promise => { + if (verifyAction?.onBefore && !verifyAction.onBefore(domain)) { + throw new BusinessError({ message: t('domain_verify.on_before') }); + } + const response = await coreClient! + .getMyOrganizationApiClient() + .organization.domains.verify.create(domain.id); + return response.status === 'verified'; + }, + onSuccess: (_, domain) => { + verifyAction?.onAfter?.(domain); + queryClient.invalidateQueries({ queryKey: domainQueryKeys.list() }); + }, + }); + + const deleteDomainMutation = useMutation({ + mutationFn: async (domain: Domain): Promise => { + if (deleteAction?.onBefore && !deleteAction.onBefore(domain)) { + throw new BusinessError({ message: t('domain_delete.on_before') }); + } + await coreClient!.getMyOrganizationApiClient().organization.domains.delete(domain.id); + }, + onSuccess: (_, domain) => { + deleteAction?.onAfter?.(domain); + queryClient.invalidateQueries({ queryKey: domainQueryKeys.list() }); + queryClient.removeQueries({ queryKey: domainQueryKeys.providers(domain.id) }); + }, + }); + + const associateToProviderMutation = useMutation({ + mutationFn: async ({ domain, provider }: { domain: Domain; provider: IdpKnownResponse }) => { + if ( + associateToProviderAction?.onBefore && + !associateToProviderAction.onBefore(domain, provider) + ) { + throw new BusinessError({ message: t('domain_associate_provider.on_before') }); + } + await coreClient! + .getMyOrganizationApiClient() + .organization.identityProviders.domains.create(provider.id!, { domain: domain.domain }); + }, + onSuccess: (_, { domain, provider }) => { + associateToProviderAction?.onAfter?.(domain, provider); + queryClient.invalidateQueries({ queryKey: domainQueryKeys.providers(domain.id) }); + }, + }); + + const deleteFromProviderMutation = useMutation({ + mutationFn: async ({ domain, provider }: { domain: Domain; provider: IdpKnownResponse }) => { + if ( + deleteFromProviderAction?.onBefore && + !deleteFromProviderAction.onBefore(domain, provider) + ) { + throw new BusinessError({ message: t('domain_delete_provider.on_before') }); + } + await coreClient! + .getMyOrganizationApiClient() + .organization.identityProviders.domains.delete(provider.id!, domain.domain); + }, + onSuccess: (_, { domain, provider }) => { + deleteFromProviderAction?.onAfter?.(domain, provider); + queryClient.invalidateQueries({ queryKey: domainQueryKeys.providers(domain.id) }); + }, + }); + + const onCreateDomain = useCallback( + (data: CreateOrganizationDomainRequestContent) => createDomainMutation.mutateAsync(data), + [createDomainMutation], + ); + + const onVerifyDomain = useCallback( + (domain: Domain) => verifyDomainMutation.mutateAsync(domain), + [verifyDomainMutation], + ); + + const onDeleteDomain = useCallback( + (domain: Domain) => deleteDomainMutation.mutateAsync(domain), + [deleteDomainMutation], + ); + + const onAssociateToProvider = useCallback( + (domain: Domain, provider: IdpKnownResponse) => + associateToProviderMutation.mutateAsync({ domain, provider }), + [associateToProviderMutation], + ); + + const onDeleteFromProvider = useCallback( + (domain: Domain, provider: IdpKnownResponse) => + deleteFromProviderMutation.mutateAsync({ domain, provider }), + [deleteFromProviderMutation], + ); + + const fetchProviders = useCallback( + async (domain: Domain) => { + setSelectedDomainId(domain.id); + setSelectedDomainName(domain.domain); + await queryClient.ensureQueryData({ + queryKey: domainQueryKeys.providers(domain.id), + queryFn: () => fetchProvidersForDomain(domain.domain), + }); + }, + [queryClient, coreClient], + ); + + const fetchDomains = useCallback(async () => { + await queryClient.getQueryData(domainQueryKeys.list()); + }, [queryClient]); + + return { + domains: domainsQuery.data ?? [], + providers: providersQuery.data ?? [], + isFetching: domainsQuery.isLoading, + isCreating: createDomainMutation.isPending, + isDeleting: deleteDomainMutation.isPending, + isVerifying: verifyDomainMutation.isPending, + isLoadingProviders: providersQuery.isLoading, + + fetchProviders, + fetchDomains, + onCreateDomain, + onVerifyDomain, + onDeleteDomain, + onAssociateToProvider, + onDeleteFromProvider, + }; +} diff --git a/packages/react/src/hooks/my-organization/use-domain-table-logic.ts b/packages/react/src/hooks/my-organization/use-domain-table-logic.ts deleted file mode 100644 index 4d9ab05f8..000000000 --- a/packages/react/src/hooks/my-organization/use-domain-table-logic.ts +++ /dev/null @@ -1,257 +0,0 @@ -/** - * Domain table UI logic hook. - * @module use-domain-table-logic - * @internal - */ - -import { type Domain, type IdpKnownResponse } from '@auth0/universal-components-core'; -import { useCallback, useEffect, useState } from 'react'; - -import { showToast } from '@/components/auth0/shared/toast'; -import { useErrorHandler } from '@/hooks/shared/use-error-handler'; -import type { - UseDomainTableLogicOptions, - UseDomainTableLogicResult, -} from '@/types/my-organization/domain-management/domain-table-types'; - -/** - * Hook for domain table modal state and action handlers. - * @param props - Component props. - * @param props.t - Translation function - * @param props.onCreateDomain - The on create domain - * @param props.onVerifyDomain - The on verify domain - * @param props.onDeleteDomain - The on delete domain - * @param props.onAssociateToProvider - The on associate to provider - * @param props.onDeleteFromProvider - The on delete from provider - * @param props.fetchProviders - The fetch providers - * @param props.fetchDomains - The fetch domains - * @internal - * @returns Hook state and methods - */ -export function useDomainTableLogic({ - t, - onCreateDomain, - onVerifyDomain, - onDeleteDomain, - onAssociateToProvider, - onDeleteFromProvider, - fetchProviders, - fetchDomains, -}: UseDomainTableLogicOptions): UseDomainTableLogicResult { - const handleError = useErrorHandler(); - const [showCreateModal, setShowCreateModal] = useState(false); - const [showConfigureModal, setShowConfigureModal] = useState(false); - const [showVerifyModal, setShowVerifyModal] = useState(false); - const [verifyError, setVerifyError] = useState(undefined); - const [showDeleteModal, setShowDeleteModal] = useState(false); - const [selectedDomain, setSelectedDomain] = useState(null); - - const handleCreate = useCallback( - async (domainUrl: string) => { - try { - const newDomain = await onCreateDomain({ domain: domainUrl }); - showToast({ - type: 'success', - message: t('domain_table.notifications.domain_create.success', { - domainName: newDomain?.domain, - }), - }); - setSelectedDomain(newDomain); - setShowCreateModal(false); - setShowVerifyModal(true); - } catch (error) { - handleError(error, { - fallbackMessage: t('domain_table.notifications.domain_create.error'), - }); - } - }, - [onCreateDomain, t, handleError], - ); - - const handleVerify = useCallback( - async (domain: Domain) => { - try { - const isVerified = await onVerifyDomain(domain); - if (isVerified) { - setShowVerifyModal(false); - showToast({ - type: 'success', - message: t('domain_table.notifications.domain_verify.success', { - domainName: domain.domain, - }), - }); - } else { - setVerifyError( - t('domain_verify.modal.errors.verification_failed', { domainName: domain.domain }), - ); - } - } catch (error) { - handleError(error, { - fallbackMessage: t('domain_table.notifications.domain_verify.error'), - }); - } - }, - [onVerifyDomain, t, handleError], - ); - - const handleDelete = useCallback( - async (domain: Domain) => { - try { - await onDeleteDomain(domain); - showToast({ - type: 'success', - message: t('domain_table.notifications.domain_delete.success', { - domainName: domain.domain, - }), - }); - setShowDeleteModal(false); - setShowVerifyModal(false); - } catch (error) { - handleError(error, { - fallbackMessage: t('domain_table.notifications.domain_delete.error'), - }); - } - }, - [onDeleteDomain, t, handleError], - ); - - const handleToggleSwitch = useCallback( - async (domain: Domain, provider: IdpKnownResponse, newCheckedValue: boolean) => { - if (newCheckedValue) { - try { - await onAssociateToProvider(domain, provider); - showToast({ - type: 'success', - message: t('domain_table.notifications.domain_associate_provider.success', { - domain: domain.domain, - idp: provider.name, - }), - }); - } catch (error) { - handleError(error, { - fallbackMessage: t('domain_table.notifications.domain_associate_provider.error'), - }); - } - } else { - try { - await onDeleteFromProvider(domain, provider); - showToast({ - type: 'success', - message: t('domain_table.notifications.domain_delete_provider.success', { - domain: domain.domain, - idp: provider.name, - }), - }); - } catch (error) { - handleError(error, { - fallbackMessage: t('domain_table.notifications.domain_delete_provider.error'), - }); - } - } - }, - [onAssociateToProvider, onDeleteFromProvider, t, handleError], - ); - - const handleCloseVerifyModal = useCallback(() => { - setShowVerifyModal(false); - setVerifyError(undefined); - }, []); - - const handleCreateClick = useCallback(() => { - setShowCreateModal(true); - }, []); - - const handleConfigureClick = useCallback( - async (domain: Domain) => { - setSelectedDomain(domain); - if (domain.status !== 'verified') { - setShowVerifyModal(true); - } else { - try { - await fetchProviders(domain); - setShowConfigureModal(true); - } catch (error) { - handleError(error, { - fallbackMessage: t('domain_table.notifications.fetch_providers_error'), - }); - } - } - }, - [fetchProviders, t, handleError], - ); - - const handleVerifyClick = useCallback( - async (domain: Domain) => { - setSelectedDomain(domain); - try { - const isVerified = await onVerifyDomain(domain); - if (isVerified) { - await fetchProviders(domain); - setShowConfigureModal(true); - showToast({ - type: 'success', - message: t('domain_table.notifications.domain_verify.success', { - domainName: domain.domain, - }), - }); - } else { - showToast({ - type: 'error', - message: t('domain_table.notifications.domain_verify.verification_failed', { - domainName: domain.domain, - }), - }); - } - } catch (error) { - handleError(error, { - fallbackMessage: t('domain_table.notifications.domain_verify.error'), - }); - } - }, - [onVerifyDomain, fetchProviders, t, handleError], - ); - - const handleDeleteClick = useCallback((domain: Domain) => { - setSelectedDomain(domain); - setShowVerifyModal(false); - setShowDeleteModal(true); - }, []); - - // Initialization - useEffect(() => { - try { - fetchDomains(); - } catch (error) { - handleError(error, { - fallbackMessage: t('domain_table.notifications.fetch_domains_error'), - }); - } - }, []); - - return { - // Modal state - showCreateModal, - showConfigureModal, - showVerifyModal, - showDeleteModal, - verifyError, - selectedDomain, - - // State setters - setShowCreateModal, - setShowConfigureModal, - setShowVerifyModal, - setShowDeleteModal, - - // Handlers - handleCreate, - handleVerify, - handleDelete, - handleToggleSwitch, - handleCloseVerifyModal, - handleCreateClick, - handleConfigureClick, - handleVerifyClick, - handleDeleteClick, - }; -} diff --git a/packages/react/src/hooks/my-organization/use-domain-table.ts b/packages/react/src/hooks/my-organization/use-domain-table.ts index 6da2f8402..afc9b2f6d 100644 --- a/packages/react/src/hooks/my-organization/use-domain-table.ts +++ b/packages/react/src/hooks/my-organization/use-domain-table.ts @@ -1,41 +1,26 @@ /** - * Domain table data and mutations hook. + * Domain table hook. + * Single public hook combining data operations and UI logic. * @module use-domain-table */ -import { - type Domain, - type IdpKnownResponse, - type CreateOrganizationDomainRequestContent, - type IdentityProviderAssociatedWithDomain, - BusinessError, -} from '@auth0/universal-components-core'; -import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; -import { useState } from 'react'; - -import { useCoreClient } from '@/hooks/shared/use-core-client'; +import { type Domain, type IdpKnownResponse } from '@auth0/universal-components-core'; +import { useCallback, useState } from 'react'; + +import { showToast } from '@/components/auth0/shared/toast'; +import { useDomainTableService } from '@/hooks/my-organization/shared/services/use-domain-table-service'; +import { useErrorHandler } from '@/hooks/shared/use-error-handler'; import { useTranslator } from '@/hooks/shared/use-translator'; import type { UseDomainTableOptions, - UseDomainTableResult, + UseDomainTableReturn, } from '@/types/my-organization/domain-management/domain-table-types'; -const domainQueryKeys = { - all: ['domains'] as const, - list: () => [...domainQueryKeys.all, 'list'] as const, - providers: (domainId: string) => [...domainQueryKeys.all, 'providers', domainId] as const, -}; - /** - * Hook for domain table data fetching and CRUD operations. - * @param props - Component props. - * @param props.createAction - Configuration for the create action - * @param props.deleteAction - Configuration for the delete action - * @param props.verifyAction - Configuration for the verify action - * @param props.associateToProviderAction - Configuration for associating to a provider - * @param props.deleteFromProviderAction - Configuration for deleting from a provider - * @param props.customMessages - Custom translation messages to override defaults - * @returns Hook state and methods + * Hook for domain table data, CRUD operations, and UI logic. + * Consumes the internal service hook and manages modal/UI state. + * @param options - Hook options including actions and custom messages. + * @returns Combined data, loading states, UI state, and handlers. */ export function useDomainTable({ createAction, @@ -44,149 +29,246 @@ export function useDomainTable({ associateToProviderAction, deleteFromProviderAction, customMessages, -}: UseDomainTableOptions): UseDomainTableResult { - const { t } = useTranslator('domain_management.domain_table.notifications', customMessages); - const { coreClient } = useCoreClient(); - const queryClient = useQueryClient(); - - const [selectedDomainId, setSelectedDomainId] = useState(null); - const [selectedDomainName, setSelectedDomainName] = useState(null); - - const fetchProvidersForDomain = async (domainName: string) => { - const api = coreClient!.getMyOrganizationApiClient(); - - const allProvidersResponse = await api.organization.identityProviders.list(); - const allProviders = allProvidersResponse?.identity_providers ?? []; - - return allProviders.map( - (provider): IdentityProviderAssociatedWithDomain => ({ - ...provider, - is_associated: provider.domains?.includes(domainName) ?? false, - }), - ); - }; +}: UseDomainTableOptions): UseDomainTableReturn { + const { t } = useTranslator('domain_management', customMessages); + const handleError = useErrorHandler(); - const domainsQuery = useQuery({ - queryKey: domainQueryKeys.list(), - queryFn: async () => { - const { response } = await coreClient! - .getMyOrganizationApiClient() - .organization.domains.list(); - return response?.organization_domains ?? []; - }, - enabled: !!coreClient, + const { + domains, + providers, + isFetching, + isCreating, + isDeleting, + isVerifying, + isLoadingProviders, + fetchProviders, + onCreateDomain, + onVerifyDomain, + onDeleteDomain, + onAssociateToProvider, + onDeleteFromProvider, + } = useDomainTableService({ + createAction, + deleteAction, + verifyAction, + associateToProviderAction, + deleteFromProviderAction, + customMessages, }); - const providersQuery = useQuery({ - queryKey: domainQueryKeys.providers(selectedDomainId ?? ''), - queryFn: () => fetchProvidersForDomain(selectedDomainName!), - enabled: !!coreClient && !!selectedDomainId && !!selectedDomainName, - }); + const [showCreateModal, setShowCreateModal] = useState(false); + const [showConfigureModal, setShowConfigureModal] = useState(false); + const [showVerifyModal, setShowVerifyModal] = useState(false); + const [verifyError, setVerifyError] = useState(undefined); + const [showDeleteModal, setShowDeleteModal] = useState(false); + const [selectedDomain, setSelectedDomain] = useState(null); - const createDomainMutation = useMutation({ - mutationFn: async (data: CreateOrganizationDomainRequestContent): Promise => { - if (createAction?.onBefore && !createAction.onBefore(data as Domain)) { - throw new BusinessError({ message: t('domain_create.on_before') }); + const handleCreate = useCallback( + async (domainUrl: string) => { + try { + const newDomain = await onCreateDomain({ domain: domainUrl }); + showToast({ + type: 'success', + message: t('domain_table.notifications.domain_create.success', { + domainName: newDomain?.domain, + }), + }); + setSelectedDomain(newDomain); + setShowCreateModal(false); + setShowVerifyModal(true); + } catch (error) { + handleError(error, { + fallbackMessage: t('domain_table.notifications.domain_create.error'), + }); } - return coreClient!.getMyOrganizationApiClient().organization.domains.create(data); - }, - onSuccess: (result) => { - createAction?.onAfter?.(result); - queryClient.invalidateQueries({ queryKey: domainQueryKeys.list() }); }, - }); + [onCreateDomain, t, handleError], + ); - const verifyDomainMutation = useMutation({ - mutationFn: async (domain: Domain): Promise => { - if (verifyAction?.onBefore && !verifyAction.onBefore(domain)) { - throw new BusinessError({ message: t('domain_verify.on_before') }); + const handleVerify = useCallback( + async (domain: Domain) => { + try { + const isVerified = await onVerifyDomain(domain); + if (isVerified) { + setShowVerifyModal(false); + showToast({ + type: 'success', + message: t('domain_table.notifications.domain_verify.success', { + domainName: domain.domain, + }), + }); + } else { + setVerifyError( + t('domain_verify.modal.errors.verification_failed', { domainName: domain.domain }), + ); + } + } catch (error) { + handleError(error, { + fallbackMessage: t('domain_table.notifications.domain_verify.error'), + }); } - const response = await coreClient! - .getMyOrganizationApiClient() - .organization.domains.verify.create(domain.id); - return response.status === 'verified'; }, - onSuccess: (_, domain) => { - verifyAction?.onAfter?.(domain); - queryClient.invalidateQueries({ queryKey: domainQueryKeys.list() }); - }, - }); + [onVerifyDomain, t, handleError], + ); - const deleteDomainMutation = useMutation({ - mutationFn: async (domain: Domain): Promise => { - if (deleteAction?.onBefore && !deleteAction.onBefore(domain)) { - throw new BusinessError({ message: t('domain_delete.on_before') }); + const handleDelete = useCallback( + async (domain: Domain) => { + try { + await onDeleteDomain(domain); + showToast({ + type: 'success', + message: t('domain_table.notifications.domain_delete.success', { + domainName: domain.domain, + }), + }); + setShowDeleteModal(false); + setShowVerifyModal(false); + } catch (error) { + handleError(error, { + fallbackMessage: t('domain_table.notifications.domain_delete.error'), + }); } - await coreClient!.getMyOrganizationApiClient().organization.domains.delete(domain.id); - }, - onSuccess: (_, domain) => { - deleteAction?.onAfter?.(domain); - queryClient.invalidateQueries({ queryKey: domainQueryKeys.list() }); - queryClient.removeQueries({ queryKey: domainQueryKeys.providers(domain.id) }); }, - }); + [onDeleteDomain, t, handleError], + ); - const associateToProviderMutation = useMutation({ - mutationFn: async ({ domain, provider }: { domain: Domain; provider: IdpKnownResponse }) => { - if ( - associateToProviderAction?.onBefore && - !associateToProviderAction.onBefore(domain, provider) - ) { - throw new BusinessError({ message: t('domain_associate_provider.on_before') }); + const handleToggleSwitch = useCallback( + async (domain: Domain, provider: IdpKnownResponse, newCheckedValue: boolean) => { + if (newCheckedValue) { + try { + await onAssociateToProvider(domain, provider); + showToast({ + type: 'success', + message: t('domain_table.notifications.domain_associate_provider.success', { + domain: domain.domain, + idp: provider.name, + }), + }); + } catch (error) { + handleError(error, { + fallbackMessage: t('domain_table.notifications.domain_associate_provider.error'), + }); + } + } else { + try { + await onDeleteFromProvider(domain, provider); + showToast({ + type: 'success', + message: t('domain_table.notifications.domain_delete_provider.success', { + domain: domain.domain, + idp: provider.name, + }), + }); + } catch (error) { + handleError(error, { + fallbackMessage: t('domain_table.notifications.domain_delete_provider.error'), + }); + } } - await coreClient! - .getMyOrganizationApiClient() - .organization.identityProviders.domains.create(provider.id!, { domain: domain.domain }); - }, - onSuccess: (_, { domain, provider }) => { - associateToProviderAction?.onAfter?.(domain, provider); - queryClient.invalidateQueries({ queryKey: domainQueryKeys.providers(domain.id) }); }, - }); + [onAssociateToProvider, onDeleteFromProvider, t, handleError], + ); - const deleteFromProviderMutation = useMutation({ - mutationFn: async ({ domain, provider }: { domain: Domain; provider: IdpKnownResponse }) => { - if ( - deleteFromProviderAction?.onBefore && - !deleteFromProviderAction.onBefore(domain, provider) - ) { - throw new BusinessError({ message: t('domain_delete_provider.on_before') }); + const handleCloseVerifyModal = useCallback(() => { + setShowVerifyModal(false); + setVerifyError(undefined); + }, []); + + const handleCreateClick = useCallback(() => { + setShowCreateModal(true); + }, []); + + const handleConfigureClick = useCallback( + async (domain: Domain) => { + setSelectedDomain(domain); + if (domain.status !== 'verified') { + setShowVerifyModal(true); + } else { + try { + await fetchProviders(domain); + setShowConfigureModal(true); + } catch (error) { + handleError(error, { + fallbackMessage: t('domain_table.notifications.fetch_providers_error'), + }); + } } - await coreClient! - .getMyOrganizationApiClient() - .organization.identityProviders.domains.delete(provider.id!, domain.domain); }, - onSuccess: (_, { domain, provider }) => { - deleteFromProviderAction?.onAfter?.(domain, provider); - queryClient.invalidateQueries({ queryKey: domainQueryKeys.providers(domain.id) }); + [fetchProviders, t, handleError], + ); + + const handleVerifyClick = useCallback( + async (domain: Domain) => { + setSelectedDomain(domain); + try { + const isVerified = await onVerifyDomain(domain); + if (isVerified) { + await fetchProviders(domain); + setShowConfigureModal(true); + showToast({ + type: 'success', + message: t('domain_table.notifications.domain_verify.success', { + domainName: domain.domain, + }), + }); + } else { + showToast({ + type: 'error', + message: t('domain_table.notifications.domain_verify.verification_failed', { + domainName: domain.domain, + }), + }); + } + } catch (error) { + handleError(error, { + fallbackMessage: t('domain_table.notifications.domain_verify.error'), + }); + } }, - }); + [onVerifyDomain, fetchProviders, t, handleError], + ); + + const handleDeleteClick = useCallback((domain: Domain) => { + setSelectedDomain(domain); + setShowVerifyModal(false); + setShowDeleteModal(true); + }, []); return { - domains: domainsQuery.data ?? [], - providers: providersQuery.data ?? [], - isFetching: domainsQuery.isLoading, - isCreating: createDomainMutation.isPending, - isDeleting: deleteDomainMutation.isPending, - isVerifying: verifyDomainMutation.isPending, - isLoadingProviders: providersQuery.isLoading, - fetchProviders: async (domain: Domain) => { - setSelectedDomainId(domain.id); - setSelectedDomainName(domain.domain); - await queryClient.ensureQueryData({ - queryKey: domainQueryKeys.providers(domain.id), - queryFn: () => fetchProvidersForDomain(domain.domain), - }); - }, - fetchDomains: async () => { - await queryClient.getQueryData(domainQueryKeys.list()); - }, - onCreateDomain: (data) => createDomainMutation.mutateAsync(data), - onVerifyDomain: (domain) => verifyDomainMutation.mutateAsync(domain), - onDeleteDomain: (domain) => deleteDomainMutation.mutateAsync(domain), - onAssociateToProvider: (domain, provider) => - associateToProviderMutation.mutateAsync({ domain, provider }), - onDeleteFromProvider: (domain, provider) => - deleteFromProviderMutation.mutateAsync({ domain, provider }), + // Data + domains, + providers, + + // Loading states + isFetching, + isCreating, + isDeleting, + isVerifying, + isLoadingProviders, + + // Modal state + showCreateModal, + showConfigureModal, + showVerifyModal, + showDeleteModal, + verifyError, + selectedDomain, + + // State setters + setShowCreateModal, + setShowConfigureModal, + setShowVerifyModal, + setShowDeleteModal, + + // Handlers + handleCreate, + handleVerify, + handleDelete, + handleToggleSwitch, + handleCloseVerifyModal, + handleCreateClick, + handleConfigureClick, + handleVerifyClick, + handleDeleteClick, }; } diff --git a/packages/react/src/tests/utils/__mocks__/my-organization/domain-management/domain.mocks.ts b/packages/react/src/tests/utils/__mocks__/my-organization/domain-management/domain.mocks.ts index d4baa2733..137512983 100644 --- a/packages/react/src/tests/utils/__mocks__/my-organization/domain-management/domain.mocks.ts +++ b/packages/react/src/tests/utils/__mocks__/my-organization/domain-management/domain.mocks.ts @@ -8,8 +8,9 @@ import { vi } from 'vitest'; import type { DomainTableProps, - UseDomainTableLogicOptions, - UseDomainTableResult, + UseDomainTableReturn, + UseDomainTableServiceOptions, + UseDomainTableServiceReturn, } from '@/types/my-organization/domain-management/domain-table-types'; export const createMockDomain = (overrides?: Partial): Domain => ({ @@ -68,7 +69,6 @@ export const createMockIdentityProviderAssociatedWithDomain = ( export const createMockIdentityProviderWithoutProvisioning = ( overrides: Partial = {}, ): IdpKnownResponse => { - // Use a strategy that doesn't have provisioning enabled by default const baseProvider = { id: 'con_abc123xyz456', name: 'mock-provider-no-provisioning', @@ -119,9 +119,9 @@ export const createMockDeleteAction = (): ComponentAction => ({ onAfter: vi.fn(), }); -export const createMockLogic = ( - overrides: Partial = {}, -) => ({ +export const createMockDomainTableReturn = ( + overrides: Partial = {}, +): UseDomainTableReturn => ({ domains: [createMockDomain(), createMockVerifiedDomain()], providers: [], isCreating: false, @@ -129,25 +129,6 @@ export const createMockLogic = ( isFetching: false, isLoadingProviders: false, isDeleting: false, - schema: undefined, - styling: { variables: { common: {}, light: {}, dark: {} }, classes: {} }, - hideHeader: false, - readOnly: false, - customMessages: {}, - createAction: undefined, - onOpenProvider: undefined, - onCreateProvider: undefined, - fetchProviders: vi.fn(), - fetchDomains: vi.fn(), - onCreateDomain: vi.fn(), - onVerifyDomain: vi.fn(), - onDeleteDomain: vi.fn(), - onAssociateToProvider: vi.fn(), - onDeleteFromProvider: vi.fn(), - ...overrides, -}); - -export const createMockApi = (overrides: Partial = {}) => ({ showCreateModal: false, showConfigureModal: false, showVerifyModal: false, @@ -156,8 +137,8 @@ export const createMockApi = (overrides: Partial = { selectedDomain: null, setShowCreateModal: vi.fn(), setShowConfigureModal: vi.fn(), - setShowDeleteModal: vi.fn(), setShowVerifyModal: vi.fn(), + setShowDeleteModal: vi.fn(), handleCreate: vi.fn(), handleVerify: vi.fn(), handleDelete: vi.fn(), @@ -169,3 +150,50 @@ export const createMockApi = (overrides: Partial = { handleDeleteClick: vi.fn(), ...overrides, }); + +export const createMockDomainTableServiceOptions = ( + overrides?: Partial, +): UseDomainTableServiceOptions => ({ + createAction: { + onBefore: vi.fn().mockReturnValue(true), + onAfter: vi.fn(), + }, + deleteAction: { + onBefore: vi.fn().mockReturnValue(true), + onAfter: vi.fn(), + }, + verifyAction: { + onBefore: vi.fn().mockReturnValue(true), + onAfter: vi.fn(), + }, + associateToProviderAction: { + onBefore: vi.fn().mockReturnValue(true), + onAfter: vi.fn(), + }, + deleteFromProviderAction: { + onBefore: vi.fn().mockReturnValue(true), + onAfter: vi.fn(), + }, + customMessages: {}, + ...overrides, +}); + +export const createMockDomainTableServiceReturn = ( + overrides: Partial = {}, +): UseDomainTableServiceReturn => ({ + domains: [], + providers: [], + isFetching: false, + isCreating: false, + isDeleting: false, + isVerifying: false, + isLoadingProviders: false, + fetchProviders: vi.fn(), + fetchDomains: vi.fn(), + onCreateDomain: vi.fn(), + onVerifyDomain: vi.fn(), + onDeleteDomain: vi.fn(), + onAssociateToProvider: vi.fn(), + onDeleteFromProvider: vi.fn(), + ...overrides, +}); diff --git a/packages/react/src/types/my-organization/domain-management/domain-table-types.ts b/packages/react/src/types/my-organization/domain-management/domain-table-types.ts index e7aa84bb4..e4da2d010 100644 --- a/packages/react/src/types/my-organization/domain-management/domain-table-types.ts +++ b/packages/react/src/types/my-organization/domain-management/domain-table-types.ts @@ -15,9 +15,9 @@ import type { DomainVerifyMessages, DomainTableMessages, CreateOrganizationDomainRequestContent, - EnhancedTranslationFunction, IdentityProviderAssociatedWithDomain, } from '@auth0/universal-components-core'; +import type React from 'react'; export type { Domain }; @@ -56,12 +56,6 @@ export interface DomainTableProps onCreateProvider?: () => void; } -// DomainTableView component props -export interface DomainTableViewProps { - logic: UseDomainTableResult & DomainTableProps; - handlers: UseDomainTableLogicResult; -} - /** Props for DomainTable actions column. */ export interface DomainTableActionsColumnProps { customMessages?: Partial; @@ -73,6 +67,7 @@ export interface DomainTableActionsColumnProps { onDelete: (domain: Domain) => void; } +/** Options for domain table hooks (shared by service and public hook). */ export interface UseDomainTableOptions { createAction?: DomainTableProps['createAction']; verifyAction?: DomainTableProps['verifyAction']; @@ -82,7 +77,11 @@ export interface UseDomainTableOptions { customMessages?: DomainTableProps['customMessages']; } -export interface UseDomainTableResult extends SharedComponentProps { +/** @internal */ +export type UseDomainTableServiceOptions = UseDomainTableOptions; + +/** Return type for the internal domain table service hook. */ +export interface UseDomainTableServiceReturn { domains: Domain[]; providers: IdentityProviderAssociatedWithDomain[]; isFetching: boolean; @@ -92,25 +91,26 @@ export interface UseDomainTableResult extends SharedComponentProps { isVerifying: boolean; fetchProviders: (domain: Domain) => Promise; fetchDomains: () => Promise; - onCreateDomain: (data: CreateOrganizationDomainRequestContent) => Promise; - onVerifyDomain: (data: Domain) => Promise; + onCreateDomain: (data: CreateOrganizationDomainRequestContent) => Promise; + onVerifyDomain: (domain: Domain) => Promise; onDeleteDomain: (domain: Domain) => Promise; onAssociateToProvider: (domain: Domain, provider: IdpKnownResponse) => Promise; onDeleteFromProvider: (domain: Domain, provider: IdpKnownResponse) => Promise; } -export interface UseDomainTableLogicOptions { - t: EnhancedTranslationFunction; - onCreateDomain: UseDomainTableResult['onCreateDomain']; - onVerifyDomain: UseDomainTableResult['onVerifyDomain']; - onDeleteDomain: UseDomainTableResult['onDeleteDomain']; - onAssociateToProvider: UseDomainTableResult['onAssociateToProvider']; - onDeleteFromProvider: UseDomainTableResult['onDeleteFromProvider']; - fetchProviders: UseDomainTableResult['fetchProviders']; - fetchDomains: UseDomainTableResult['fetchDomains']; -} +/** Return type for the public domain table hook. */ +export interface UseDomainTableReturn { + // Data + domains: Domain[]; + providers: IdentityProviderAssociatedWithDomain[]; + + // Loading states + isFetching: boolean; + isCreating: boolean; + isDeleting: boolean; + isVerifying: boolean; + isLoadingProviders: boolean; -export interface UseDomainTableLogicResult { // Modal state showCreateModal: boolean; showConfigureModal: boolean; @@ -120,19 +120,36 @@ export interface UseDomainTableLogicResult { selectedDomain: Domain | null; // State setters - setShowCreateModal: (show: boolean) => void; - setShowConfigureModal: (show: boolean) => void; - setShowVerifyModal: (show: boolean) => void; - setShowDeleteModal: (show: boolean) => void; + setShowCreateModal: React.Dispatch>; + setShowConfigureModal: React.Dispatch>; + setShowVerifyModal: React.Dispatch>; + setShowDeleteModal: React.Dispatch>; // Handlers handleCreate: (domainUrl: string) => Promise; handleVerify: (domain: Domain) => Promise; - handleDelete: (domain: Domain) => void; - handleToggleSwitch: (domain: Domain, provider: IdpKnownResponse, checked: boolean) => void; + handleDelete: (domain: Domain) => Promise; + handleToggleSwitch: ( + domain: Domain, + provider: IdpKnownResponse, + checked: boolean, + ) => Promise; handleCloseVerifyModal: () => void; handleCreateClick: () => void; handleConfigureClick: (domain: Domain) => void; handleVerifyClick: (domain: Domain) => Promise; handleDeleteClick: (domain: Domain) => void; } + +/** Props for the DomainTableView presentational component. @internal */ +export interface DomainTableViewProps { + domainTable: UseDomainTableReturn; + schema: DomainTableProps['schema']; + styling: DomainTableProps['styling']; + hideHeader: DomainTableProps['hideHeader']; + readOnly: DomainTableProps['readOnly']; + customMessages: DomainTableProps['customMessages']; + createAction: DomainTableProps['createAction']; + onOpenProvider: DomainTableProps['onOpenProvider']; + onCreateProvider: DomainTableProps['onCreateProvider']; +}