diff --git a/packages/react/src/components/auth0/my-organization/shared/member-management/invitations/invitation-details/__tests__/organization-invitation-details-modal.test.tsx b/packages/react/src/components/auth0/my-organization/shared/member-management/invitations/invitation-details/__tests__/organization-invitation-details-modal.test.tsx
new file mode 100644
index 000000000..266f128cc
--- /dev/null
+++ b/packages/react/src/components/auth0/my-organization/shared/member-management/invitations/invitation-details/__tests__/organization-invitation-details-modal.test.tsx
@@ -0,0 +1,386 @@
+import { screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { vi, describe, it, expect, afterEach } from 'vitest';
+
+import { OrganizationInvitationDetailsModal } from '@/components/auth0/my-organization/shared/member-management/invitations/invitation-details/organization-invitation-details-modal';
+import { renderWithProviders } from '@/tests/utils';
+import {
+ createMockDetailsModalProps,
+ createMockInvitation,
+ createMockPendingInvitation,
+ createMockExpiredInvitation,
+ createMockRoles,
+ createMockProviders,
+} from '@/tests/utils/__mocks__/my-organization/member-management/invitation.mocks';
+
+describe('OrganizationInvitationDetailsModal', () => {
+ afterEach(() => {
+ vi.clearAllMocks();
+ });
+
+ describe('isOpen', () => {
+ describe('when is true', () => {
+ it('should render the modal', () => {
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByRole('dialog')).toBeInTheDocument();
+ expect(
+ screen.getByRole('heading', { name: 'invitation.details.title' }),
+ ).toBeInTheDocument();
+ });
+ });
+
+ describe('when is false', () => {
+ it('should not render the modal content', () => {
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
+ });
+ });
+ });
+
+ describe('invitation', () => {
+ describe('when invitation is provided', () => {
+ it('should display the invitee email', () => {
+ const invitation = createMockInvitation({ invitee: { email: 'user@example.com' } });
+
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByDisplayValue('user@example.com')).toBeInTheDocument();
+ });
+
+ it('should display the inviter name', () => {
+ const invitation = createMockInvitation({ inviter: { name: 'John Doe' } });
+
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByDisplayValue('John Doe')).toBeInTheDocument();
+ });
+
+ it('should display created_at date', () => {
+ const invitation = createMockInvitation({
+ created_at: '2024-06-15T10:00:00.000Z',
+ });
+
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByText('invitation.details.created_at_label')).toBeInTheDocument();
+ });
+
+ it('should display expires_at date', () => {
+ const invitation = createMockInvitation({
+ expires_at: '2025-06-15T10:00:00.000Z',
+ });
+
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByText('invitation.details.expires_at_label')).toBeInTheDocument();
+ });
+ });
+
+ describe('when invitation is null', () => {
+ it('should handle null invitation gracefully', () => {
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByRole('dialog')).toBeInTheDocument();
+ });
+ });
+ });
+
+ describe('status badge', () => {
+ it('should display pending status for pending invitations', () => {
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByText('invitation.table.status_pending')).toBeInTheDocument();
+ });
+
+ it('should display expired status for expired invitations', () => {
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByText('invitation.table.status_expired')).toBeInTheDocument();
+ });
+ });
+
+ describe('roles', () => {
+ it('should resolve role IDs to names when availableRoles provided', () => {
+ const invitation = createMockInvitation({ roles: ['role_admin', 'role_member'] });
+ const availableRoles = createMockRoles();
+
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByText('Admin')).toBeInTheDocument();
+ expect(screen.getByText('Member')).toBeInTheDocument();
+ });
+
+ it('should show role ID as fallback when role not found in availableRoles', () => {
+ const invitation = createMockInvitation({ roles: ['role_unknown'] });
+
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByText('role_unknown')).toBeInTheDocument();
+ });
+
+ it('should show dash when no roles assigned', () => {
+ const invitation = createMockInvitation({ roles: [] });
+
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByText('invitation.details.roles_label')).toBeInTheDocument();
+ });
+ });
+
+ describe('invitation URL', () => {
+ it('should display invitation URL when available', () => {
+ const invitation = createMockInvitation({
+ invitation_url: 'https://example.auth0.com/invite?ticket=abc',
+ });
+
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByText('invitation.details.invitation_url_label')).toBeInTheDocument();
+ });
+
+ it('should not display invitation URL section when no URL', () => {
+ const invitation = createMockInvitation({ invitation_url: undefined });
+
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.queryByText('invitation.details.invitation_url_label')).not.toBeInTheDocument();
+ });
+ });
+
+ describe('identity provider', () => {
+ it('should display provider name when resolved', () => {
+ const invitation = createMockInvitation({ identity_provider_id: 'con_provider1' });
+ const availableProviders = createMockProviders();
+
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByDisplayValue('Google')).toBeInTheDocument();
+ });
+
+ it('should show provider ID as fallback when provider not found', () => {
+ const invitation = createMockInvitation({ identity_provider_id: 'con_unknown' });
+
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByDisplayValue('con_unknown')).toBeInTheDocument();
+ });
+
+ it('should not display provider section when no provider assigned', () => {
+ const invitation = createMockInvitation({ identity_provider_id: undefined });
+
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.queryByText('invitation.details.provider_label')).not.toBeInTheDocument();
+ });
+ });
+
+ describe('readOnly', () => {
+ describe('when readOnly is false', () => {
+ it('should show Revoke and Resend buttons', () => {
+ renderWithProviders(
+ ,
+ );
+
+ expect(
+ screen.getByRole('button', { name: 'invitation.details.revoke_button' }),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByRole('button', { name: 'invitation.details.resend_button' }),
+ ).toBeInTheDocument();
+ });
+ });
+
+ describe('when readOnly is true', () => {
+ it('should not show Revoke and Resend buttons', () => {
+ renderWithProviders(
+ ,
+ );
+
+ expect(
+ screen.queryByRole('button', { name: 'invitation.details.revoke_button' }),
+ ).not.toBeInTheDocument();
+ expect(
+ screen.queryByRole('button', { name: 'invitation.details.resend_button' }),
+ ).not.toBeInTheDocument();
+ });
+ });
+ });
+
+ describe('action callbacks', () => {
+ it('should call onRevoke when Revoke button is clicked', async () => {
+ const user = userEvent.setup();
+ const onRevoke = vi.fn();
+ const invitation = createMockPendingInvitation();
+
+ renderWithProviders(
+ ,
+ );
+
+ const revokeButton = screen.getByRole('button', {
+ name: 'invitation.details.revoke_button',
+ });
+ await user.click(revokeButton);
+
+ expect(onRevoke).toHaveBeenCalledTimes(1);
+ expect(onRevoke).toHaveBeenCalledWith(invitation);
+ });
+
+ it('should call onResend when Resend button is clicked', async () => {
+ const user = userEvent.setup();
+ const onResend = vi.fn();
+ const invitation = createMockPendingInvitation();
+
+ renderWithProviders(
+ ,
+ );
+
+ const resendButton = screen.getByRole('button', {
+ name: 'invitation.details.resend_button',
+ });
+ await user.click(resendButton);
+
+ expect(onResend).toHaveBeenCalledTimes(1);
+ expect(onResend).toHaveBeenCalledWith(invitation);
+ });
+
+ it('should call onClose when Close button is clicked', async () => {
+ const user = userEvent.setup();
+ const onClose = vi.fn();
+
+ renderWithProviders(
+ ,
+ );
+
+ const closeButton = screen.getByRole('button', {
+ name: 'invitation.details.close_button',
+ });
+ await user.click(closeButton);
+
+ expect(onClose).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('action in progress', () => {
+ it('should disable Revoke button when isRevoking is true', () => {
+ renderWithProviders(
+ ,
+ );
+
+ const revokeButton = screen.getByRole('button', {
+ name: 'invitation.details.revoke_button',
+ });
+ expect(revokeButton).toBeDisabled();
+ });
+
+ it('should disable Resend button when isResending is true', () => {
+ renderWithProviders(
+ ,
+ );
+
+ const resendButton = screen.getByRole('button', {
+ name: 'invitation.details.resend_button',
+ });
+ expect(resendButton).toBeDisabled();
+ });
+
+ it('should disable both buttons when either action is in progress', () => {
+ renderWithProviders(
+ ,
+ );
+
+ const revokeButton = screen.getByRole('button', {
+ name: 'invitation.details.revoke_button',
+ });
+ const resendButton = screen.getByRole('button', {
+ name: 'invitation.details.resend_button',
+ });
+ expect(revokeButton).toBeDisabled();
+ expect(resendButton).toBeDisabled();
+ });
+ });
+
+ describe('className', () => {
+ it('should apply custom class to modal', () => {
+ const customClass = 'custom-details-class';
+
+ renderWithProviders(
+ ,
+ );
+
+ const modalContent = document.querySelector('[data-slot="dialog-content"]');
+ expect(modalContent).toHaveClass(customClass);
+ });
+ });
+});
diff --git a/packages/react/src/components/auth0/my-organization/shared/member-management/invitations/invitation-revoke/__tests__/organization-invitation-revoke-modal.test.tsx b/packages/react/src/components/auth0/my-organization/shared/member-management/invitations/invitation-revoke/__tests__/organization-invitation-revoke-modal.test.tsx
new file mode 100644
index 000000000..595076fa3
--- /dev/null
+++ b/packages/react/src/components/auth0/my-organization/shared/member-management/invitations/invitation-revoke/__tests__/organization-invitation-revoke-modal.test.tsx
@@ -0,0 +1,246 @@
+import { screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { vi, describe, it, expect, afterEach } from 'vitest';
+
+import { OrganizationInvitationRevokeModal } from '@/components/auth0/my-organization/shared/member-management/invitations/invitation-revoke/organization-invitation-revoke-modal';
+import { renderWithProviders } from '@/tests/utils';
+import {
+ createMockRevokeModalProps,
+ createMockPendingInvitation,
+} from '@/tests/utils/__mocks__/my-organization/member-management/invitation.mocks';
+
+describe('OrganizationInvitationRevokeModal', () => {
+ afterEach(() => {
+ vi.clearAllMocks();
+ });
+
+ describe('isOpen', () => {
+ describe('when is true', () => {
+ it('should render the modal', () => {
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByRole('dialog')).toBeInTheDocument();
+ });
+ });
+
+ describe('when is false', () => {
+ it('should not render the modal content', () => {
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
+ });
+ });
+ });
+
+ describe('revoke mode', () => {
+ describe('when isRevokeAndResend is false', () => {
+ it('should render revoke-specific title', () => {
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByText('invitation.revoke.title')).toBeInTheDocument();
+ });
+
+ it('should render revoke-specific description', () => {
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByText('invitation.revoke.description')).toBeInTheDocument();
+ });
+
+ it('should render revoke-specific button text', () => {
+ renderWithProviders(
+ ,
+ );
+
+ expect(
+ screen.getByRole('button', { name: 'invitation.revoke.confirm_button' }),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByRole('button', { name: 'invitation.revoke.cancel_button' }),
+ ).toBeInTheDocument();
+ });
+ });
+
+ describe('when isRevokeAndResend is true', () => {
+ it('should render revoke-and-resend title', () => {
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByText('invitation.revoke_resend.title')).toBeInTheDocument();
+ });
+
+ it('should render revoke-and-resend description', () => {
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByText('invitation.revoke_resend.description')).toBeInTheDocument();
+ });
+
+ it('should render revoke-and-resend button text', () => {
+ renderWithProviders(
+ ,
+ );
+
+ expect(
+ screen.getByRole('button', { name: 'invitation.revoke_resend.confirm_button' }),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByRole('button', { name: 'invitation.revoke_resend.cancel_button' }),
+ ).toBeInTheDocument();
+ });
+ });
+ });
+
+ describe('isLoading', () => {
+ describe('when is true', () => {
+ it('should disable confirm button', () => {
+ renderWithProviders(
+ ,
+ );
+
+ const confirmButton = screen.getByRole('button', {
+ name: 'invitation.revoke.confirm_button',
+ });
+ expect(confirmButton).toBeDisabled();
+ });
+
+ it('should disable cancel button', () => {
+ renderWithProviders(
+ ,
+ );
+
+ const cancelButton = screen.getByRole('button', {
+ name: 'invitation.revoke.cancel_button',
+ });
+ expect(cancelButton).toBeDisabled();
+ });
+ });
+
+ describe('when is false', () => {
+ it('should enable confirm button', () => {
+ renderWithProviders(
+ ,
+ );
+
+ const confirmButton = screen.getByRole('button', {
+ name: 'invitation.revoke.confirm_button',
+ });
+ expect(confirmButton).toBeEnabled();
+ });
+ });
+ });
+
+ describe('onConfirm', () => {
+ it('should call onConfirm with invitation when confirm button is clicked', async () => {
+ const user = userEvent.setup();
+ const onConfirm = vi.fn();
+ const invitation = createMockPendingInvitation();
+
+ renderWithProviders(
+ ,
+ );
+
+ const confirmButton = screen.getByRole('button', {
+ name: 'invitation.revoke.confirm_button',
+ });
+ await user.click(confirmButton);
+
+ expect(onConfirm).toHaveBeenCalledTimes(1);
+ expect(onConfirm).toHaveBeenCalledWith(invitation);
+ });
+
+ it('should not call onConfirm when invitation is null', async () => {
+ const user = userEvent.setup();
+ const onConfirm = vi.fn();
+
+ renderWithProviders(
+ ,
+ );
+
+ const confirmButton = screen.getByRole('button', {
+ name: 'invitation.revoke.confirm_button',
+ });
+ await user.click(confirmButton);
+
+ expect(onConfirm).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('onClose', () => {
+ it('should call onClose when cancel button is clicked', async () => {
+ const user = userEvent.setup();
+ const onClose = vi.fn();
+
+ renderWithProviders(
+ ,
+ );
+
+ const cancelButton = screen.getByRole('button', {
+ name: 'invitation.revoke.cancel_button',
+ });
+ await user.click(cancelButton);
+
+ expect(onClose).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('className', () => {
+ it('should apply custom class to modal', () => {
+ const customClass = 'custom-revoke-class';
+
+ renderWithProviders(
+ ,
+ );
+
+ const modalContent = document.querySelector('[data-slot="dialog-content"]');
+ expect(modalContent).toHaveClass(customClass);
+ });
+ });
+
+ describe('invitation', () => {
+ describe('when invitation is null', () => {
+ it('should handle null invitation gracefully', () => {
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByRole('dialog')).toBeInTheDocument();
+ });
+ });
+ });
+});
diff --git a/packages/react/src/components/auth0/my-organization/shared/member-management/invitations/invitation-table/__tests__/organization-invitation-table-actions-column.test.tsx b/packages/react/src/components/auth0/my-organization/shared/member-management/invitations/invitation-table/__tests__/organization-invitation-table-actions-column.test.tsx
new file mode 100644
index 000000000..ee819768e
--- /dev/null
+++ b/packages/react/src/components/auth0/my-organization/shared/member-management/invitations/invitation-table/__tests__/organization-invitation-table-actions-column.test.tsx
@@ -0,0 +1,321 @@
+import { screen, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+
+import { OrganizationInvitationTableActionsColumn } from '@/components/auth0/my-organization/shared/member-management/invitations/invitation-table/organization-invitation-table-actions-column';
+import { renderWithProviders } from '@/tests/utils';
+import {
+ createMockActionsColumnProps,
+ createMockPendingInvitation,
+ createMockExpiredInvitation,
+} from '@/tests/utils/__mocks__/my-organization/member-management/invitation.mocks';
+
+describe('OrganizationInvitationTableActionsColumn', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ describe('Rendering and Basic Structure', () => {
+ it('should render dropdown trigger button', () => {
+ const props = createMockActionsColumnProps();
+ renderWithProviders();
+
+ const trigger = screen.getByRole('button');
+ expect(trigger).toBeInTheDocument();
+ expect(trigger).toHaveClass('h-8', 'w-8');
+ });
+
+ it('should have proper accessibility attributes', () => {
+ const props = createMockActionsColumnProps();
+ renderWithProviders();
+
+ const trigger = screen.getByRole('button');
+ expect(trigger).toHaveAttribute('type', 'button');
+ });
+ });
+
+ describe('Dropdown Menu Interactions', () => {
+ it('should open dropdown menu when trigger button is clicked', async () => {
+ const user = userEvent.setup();
+ const props = createMockActionsColumnProps();
+ renderWithProviders();
+
+ const trigger = screen.getByRole('button');
+ await user.click(trigger);
+
+ expect(
+ screen.getByRole('menuitem', { name: 'invitation.actions.view_details' }),
+ ).toBeInTheDocument();
+ });
+
+ it('should close dropdown menu when user presses Escape key', async () => {
+ const user = userEvent.setup();
+ const props = createMockActionsColumnProps();
+ renderWithProviders();
+
+ const trigger = screen.getByRole('button');
+ await user.click(trigger);
+
+ expect(
+ screen.getByRole('menuitem', { name: 'invitation.actions.view_details' }),
+ ).toBeInTheDocument();
+
+ await user.keyboard('{Escape}');
+
+ await waitFor(() => {
+ expect(
+ screen.queryByRole('menuitem', { name: 'invitation.actions.view_details' }),
+ ).not.toBeInTheDocument();
+ });
+ });
+ });
+
+ describe('Invitation Status: Pending', () => {
+ it('should show View Details action', async () => {
+ const user = userEvent.setup();
+ const props = createMockActionsColumnProps({
+ invitation: createMockPendingInvitation(),
+ });
+ renderWithProviders();
+
+ const trigger = screen.getByRole('button');
+ await user.click(trigger);
+
+ expect(
+ screen.getByRole('menuitem', { name: 'invitation.actions.view_details' }),
+ ).toBeInTheDocument();
+ });
+
+ it('should show Copy URL action when invitation has URL', async () => {
+ const user = userEvent.setup();
+ const props = createMockActionsColumnProps({
+ invitation: createMockPendingInvitation({
+ invitation_url: 'https://example.com/invite?ticket=abc',
+ }),
+ });
+ renderWithProviders();
+
+ const trigger = screen.getByRole('button');
+ await user.click(trigger);
+
+ expect(
+ screen.getByRole('menuitem', { name: 'invitation.actions.copy_url' }),
+ ).toBeInTheDocument();
+ });
+
+ it('should not show Copy URL action when invitation has no URL', async () => {
+ const user = userEvent.setup();
+ const props = createMockActionsColumnProps({
+ invitation: createMockPendingInvitation({ invitation_url: undefined }),
+ });
+ renderWithProviders();
+
+ const trigger = screen.getByRole('button');
+ await user.click(trigger);
+
+ expect(
+ screen.queryByRole('menuitem', { name: 'invitation.actions.copy_url' }),
+ ).not.toBeInTheDocument();
+ });
+
+ it('should show Revoke & Resend action when not readOnly', async () => {
+ const user = userEvent.setup();
+ const props = createMockActionsColumnProps({ readOnly: false });
+ renderWithProviders();
+
+ const trigger = screen.getByRole('button');
+ await user.click(trigger);
+
+ expect(
+ screen.getByRole('menuitem', { name: 'invitation.actions.revoke_and_resend' }),
+ ).toBeInTheDocument();
+ });
+
+ it('should show Revoke action when not readOnly', async () => {
+ const user = userEvent.setup();
+ const props = createMockActionsColumnProps({ readOnly: false });
+ renderWithProviders();
+
+ const trigger = screen.getByRole('button');
+ await user.click(trigger);
+
+ expect(
+ screen.getByRole('menuitem', { name: 'invitation.actions.revoke' }),
+ ).toBeInTheDocument();
+ });
+ });
+
+ describe('Invitation Status: Expired', () => {
+ it('should show View Details action', async () => {
+ const user = userEvent.setup();
+ const props = createMockActionsColumnProps({
+ invitation: createMockExpiredInvitation(),
+ });
+ renderWithProviders();
+
+ const trigger = screen.getByRole('button');
+ await user.click(trigger);
+
+ expect(
+ screen.getByRole('menuitem', { name: 'invitation.actions.view_details' }),
+ ).toBeInTheDocument();
+ });
+
+ it('should not show Copy URL action for expired invitations', async () => {
+ const user = userEvent.setup();
+ const props = createMockActionsColumnProps({
+ invitation: createMockExpiredInvitation(),
+ });
+ renderWithProviders();
+
+ const trigger = screen.getByRole('button');
+ await user.click(trigger);
+
+ expect(
+ screen.queryByRole('menuitem', { name: 'invitation.actions.copy_url' }),
+ ).not.toBeInTheDocument();
+ });
+ });
+
+ describe('Read-Only Mode', () => {
+ it('should not show Revoke & Resend action when readOnly', async () => {
+ const user = userEvent.setup();
+ const props = createMockActionsColumnProps({ readOnly: true });
+ renderWithProviders();
+
+ const trigger = screen.getByRole('button');
+ await user.click(trigger);
+
+ expect(
+ screen.queryByRole('menuitem', { name: 'invitation.actions.revoke_and_resend' }),
+ ).not.toBeInTheDocument();
+ });
+
+ it('should not show Revoke action when readOnly', async () => {
+ const user = userEvent.setup();
+ const props = createMockActionsColumnProps({ readOnly: true });
+ renderWithProviders();
+
+ const trigger = screen.getByRole('button');
+ await user.click(trigger);
+
+ expect(
+ screen.queryByRole('menuitem', { name: 'invitation.actions.revoke' }),
+ ).not.toBeInTheDocument();
+ });
+
+ it('should still show View Details when readOnly', async () => {
+ const user = userEvent.setup();
+ const props = createMockActionsColumnProps({ readOnly: true });
+ renderWithProviders();
+
+ const trigger = screen.getByRole('button');
+ await user.click(trigger);
+
+ expect(
+ screen.getByRole('menuitem', { name: 'invitation.actions.view_details' }),
+ ).toBeInTheDocument();
+ });
+ });
+
+ describe('Callback Invocations', () => {
+ it('should call onViewDetails when View Details is clicked', async () => {
+ const user = userEvent.setup();
+ const onViewDetails = vi.fn();
+ const invitation = createMockPendingInvitation();
+ const props = createMockActionsColumnProps({ invitation, onViewDetails });
+ renderWithProviders();
+
+ const trigger = screen.getByRole('button');
+ await user.click(trigger);
+
+ const menuItem = screen.getByRole('menuitem', {
+ name: 'invitation.actions.view_details',
+ });
+ await user.click(menuItem);
+
+ expect(onViewDetails).toHaveBeenCalledTimes(1);
+ expect(onViewDetails).toHaveBeenCalledWith(invitation);
+ });
+
+ it('should call onCopyUrl when Copy URL is clicked', async () => {
+ const user = userEvent.setup();
+ const onCopyUrl = vi.fn();
+ const invitation = createMockPendingInvitation({
+ invitation_url: 'https://example.com/invite',
+ });
+ const props = createMockActionsColumnProps({ invitation, onCopyUrl });
+ renderWithProviders();
+
+ const trigger = screen.getByRole('button');
+ await user.click(trigger);
+
+ const menuItem = screen.getByRole('menuitem', {
+ name: 'invitation.actions.copy_url',
+ });
+ await user.click(menuItem);
+
+ expect(onCopyUrl).toHaveBeenCalledTimes(1);
+ expect(onCopyUrl).toHaveBeenCalledWith(invitation);
+ });
+
+ it('should call onRevokeAndResend when Revoke & Resend is clicked', async () => {
+ const user = userEvent.setup();
+ const onRevokeAndResend = vi.fn();
+ const invitation = createMockPendingInvitation();
+ const props = createMockActionsColumnProps({ invitation, onRevokeAndResend });
+ renderWithProviders();
+
+ const trigger = screen.getByRole('button');
+ await user.click(trigger);
+
+ const menuItem = screen.getByRole('menuitem', {
+ name: 'invitation.actions.revoke_and_resend',
+ });
+ await user.click(menuItem);
+
+ expect(onRevokeAndResend).toHaveBeenCalledTimes(1);
+ expect(onRevokeAndResend).toHaveBeenCalledWith(invitation);
+ });
+
+ it('should call onRevoke when Revoke is clicked', async () => {
+ const user = userEvent.setup();
+ const onRevoke = vi.fn();
+ const invitation = createMockPendingInvitation();
+ const props = createMockActionsColumnProps({ invitation, onRevoke });
+ renderWithProviders();
+
+ const trigger = screen.getByRole('button');
+ await user.click(trigger);
+
+ const menuItem = screen.getByRole('menuitem', {
+ name: 'invitation.actions.revoke',
+ });
+ await user.click(menuItem);
+
+ expect(onRevoke).toHaveBeenCalledTimes(1);
+ expect(onRevoke).toHaveBeenCalledWith(invitation);
+ });
+ });
+
+ describe('Custom Messages', () => {
+ it('should accept custom messages prop without error', async () => {
+ const user = userEvent.setup();
+ const customMessages = {
+ actions: {
+ view_details: 'Custom View Details',
+ },
+ };
+ const props = createMockActionsColumnProps({ customMessages });
+ renderWithProviders();
+
+ const trigger = screen.getByRole('button');
+ await user.click(trigger);
+
+ // The mock translator returns keys, so verify the menu item renders
+ expect(
+ screen.getByRole('menuitem', { name: 'invitation.actions.view_details' }),
+ ).toBeInTheDocument();
+ });
+ });
+});
diff --git a/packages/react/src/components/auth0/my-organization/shared/member-management/shared/invitation-create/__tests__/organization-invitation-create-modal.test.tsx b/packages/react/src/components/auth0/my-organization/shared/member-management/shared/invitation-create/__tests__/organization-invitation-create-modal.test.tsx
new file mode 100644
index 000000000..d35fcca86
--- /dev/null
+++ b/packages/react/src/components/auth0/my-organization/shared/member-management/shared/invitation-create/__tests__/organization-invitation-create-modal.test.tsx
@@ -0,0 +1,218 @@
+import { screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { vi, describe, it, expect, afterEach } from 'vitest';
+
+import { OrganizationInvitationCreateModal } from '@/components/auth0/my-organization/shared/member-management/shared/invitation-create/organization-invitation-create-modal';
+import { renderWithProviders } from '@/tests/utils';
+import {
+ createMockCreateModalProps,
+ createMockRoles,
+ createMockProviders,
+} from '@/tests/utils/__mocks__/my-organization/member-management/invitation.mocks';
+
+describe('OrganizationInvitationCreateModal', () => {
+ afterEach(() => {
+ vi.clearAllMocks();
+ });
+
+ describe('isOpen', () => {
+ describe('when is true', () => {
+ it('should render the modal', () => {
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByRole('dialog')).toBeInTheDocument();
+ expect(screen.getByText('invitation.create.title')).toBeInTheDocument();
+ });
+ });
+
+ describe('when is false', () => {
+ it('should not render the modal content', () => {
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
+ });
+ });
+ });
+
+ describe('isLoading', () => {
+ describe('when is true', () => {
+ it('should disable form inputs', () => {
+ renderWithProviders(
+ ,
+ );
+
+ const emailInput = screen.getByPlaceholderText('invitation.create.email_placeholder');
+ expect(emailInput).toBeDisabled();
+ });
+
+ it('should disable cancel and submit buttons', () => {
+ renderWithProviders(
+ ,
+ );
+
+ const cancelButton = screen.getByRole('button', {
+ name: 'invitation.create.cancel_button',
+ });
+ expect(cancelButton).toBeDisabled();
+ });
+ });
+
+ describe('when is false', () => {
+ it('should enable form inputs', () => {
+ renderWithProviders(
+ ,
+ );
+
+ const emailInput = screen.getByPlaceholderText('invitation.create.email_placeholder');
+ expect(emailInput).toBeEnabled();
+ });
+ });
+ });
+
+ describe('className', () => {
+ describe('when className is provided', () => {
+ it('should apply custom class to modal', () => {
+ const customClass = 'custom-modal-class';
+
+ renderWithProviders(
+ ,
+ );
+
+ const modalContent = document.querySelector('[data-slot="dialog-content"]');
+ expect(modalContent).toHaveClass(customClass);
+ });
+ });
+ });
+
+ describe('onClose', () => {
+ describe('when modal is closed', () => {
+ it('should call onClose callback via cancel button', async () => {
+ const user = userEvent.setup();
+ const mockOnClose = vi.fn();
+
+ renderWithProviders(
+ ,
+ );
+
+ const cancelButton = screen.getByRole('button', {
+ name: 'invitation.create.cancel_button',
+ });
+ await user.click(cancelButton);
+
+ expect(mockOnClose).toHaveBeenCalledTimes(1);
+ });
+ });
+ });
+
+ describe('email input', () => {
+ it('should render email input field', () => {
+ renderWithProviders();
+
+ expect(
+ screen.getByPlaceholderText('invitation.create.email_placeholder'),
+ ).toBeInTheDocument();
+ expect(screen.getByText(/invitation\.create\.email_label/)).toBeInTheDocument();
+ });
+
+ it('should show helper text by default', () => {
+ renderWithProviders();
+
+ expect(screen.getByText('invitation.create.email_helper')).toBeInTheDocument();
+ });
+ });
+
+ describe('submit', () => {
+ it('should disable submit button when no emails are added', () => {
+ renderWithProviders();
+
+ const submitButton = screen.getByRole('button', {
+ name: 'invitation.create.submit_button',
+ });
+ expect(submitButton).toBeDisabled();
+ });
+
+ it('should show creating text when isLoading is true', () => {
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByText('invitation.create.creating')).toBeInTheDocument();
+ });
+ });
+
+ describe('availableRoles', () => {
+ describe('when roles are provided', () => {
+ it('should render roles combobox', () => {
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByText('invitation.create.roles_label')).toBeInTheDocument();
+ });
+ });
+
+ describe('when no roles are provided', () => {
+ it('should still render roles section', () => {
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByText('invitation.create.roles_label')).toBeInTheDocument();
+ });
+ });
+ });
+
+ describe('availableProviders', () => {
+ describe('when providers are provided', () => {
+ it('should render provider dropdown', () => {
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByText('invitation.create.provider_label')).toBeInTheDocument();
+ });
+ });
+
+ describe('when no providers are provided', () => {
+ it('should still render provider section', () => {
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByText('invitation.create.provider_label')).toBeInTheDocument();
+ });
+ });
+ });
+
+ describe('description', () => {
+ it('should render description text', () => {
+ renderWithProviders();
+
+ expect(screen.getByText('invitation.create.description')).toBeInTheDocument();
+ });
+ });
+});
diff --git a/packages/react/src/components/auth0/my-organization/shared/member-management/shared/search-filter/__tests__/search-filter.test.tsx b/packages/react/src/components/auth0/my-organization/shared/member-management/shared/search-filter/__tests__/search-filter.test.tsx
new file mode 100644
index 000000000..7b83b6a4d
--- /dev/null
+++ b/packages/react/src/components/auth0/my-organization/shared/member-management/shared/search-filter/__tests__/search-filter.test.tsx
@@ -0,0 +1,103 @@
+import { screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { vi, describe, it, expect, afterEach } from 'vitest';
+
+import { SearchFilter } from '@/components/auth0/my-organization/shared/member-management/shared/search-filter/search-filter';
+import { renderWithProviders } from '@/tests/utils';
+import { createMockSearchFilterProps } from '@/tests/utils/__mocks__/my-organization/member-management/invitation.mocks';
+
+describe('SearchFilter', () => {
+ afterEach(() => {
+ vi.clearAllMocks();
+ });
+
+ describe('rendering', () => {
+ it('should render the filter when roles are provided', () => {
+ renderWithProviders();
+
+ expect(screen.getByText(/invitation\.table\.filter_by_role/)).toBeInTheDocument();
+ });
+
+ it('should return null when no roles are provided', () => {
+ const { container } = renderWithProviders(
+ ,
+ );
+
+ expect(container.innerHTML).toBe('');
+ });
+
+ it('should render reset button', () => {
+ renderWithProviders();
+
+ expect(
+ screen.getByRole('button', { name: 'invitation.table.reset_filter' }),
+ ).toBeInTheDocument();
+ });
+ });
+
+ describe('reset button', () => {
+ it('should be disabled when no active filter', () => {
+ renderWithProviders();
+
+ const resetButton = screen.getByRole('button', {
+ name: 'invitation.table.reset_filter',
+ });
+ expect(resetButton).toBeDisabled();
+ });
+
+ it('should be enabled when there is an active filter', () => {
+ renderWithProviders(
+ ,
+ );
+
+ const resetButton = screen.getByRole('button', {
+ name: 'invitation.table.reset_filter',
+ });
+ expect(resetButton).toBeEnabled();
+ });
+
+ it('should call onRoleFilterChange with undefined when reset is clicked', async () => {
+ const user = userEvent.setup();
+ const onRoleFilterChange = vi.fn();
+
+ renderWithProviders(
+ ,
+ );
+
+ const resetButton = screen.getByRole('button', {
+ name: 'invitation.table.reset_filter',
+ });
+ await user.click(resetButton);
+
+ expect(onRoleFilterChange).toHaveBeenCalledTimes(1);
+ expect(onRoleFilterChange).toHaveBeenCalledWith(undefined);
+ });
+ });
+
+ describe('className', () => {
+ it('should apply custom class when provided', () => {
+ const customClass = 'custom-filter-class';
+
+ const { container } = renderWithProviders(
+ ,
+ );
+
+ const filterDiv = container.firstChild as HTMLElement;
+ expect(filterDiv).toHaveClass(customClass);
+ });
+
+ it('should apply default class when no custom class provided', () => {
+ const { container } = renderWithProviders(
+ ,
+ );
+
+ const filterDiv = container.firstChild as HTMLElement;
+ expect(filterDiv).toHaveClass('mb-4');
+ });
+ });
+});
diff --git a/packages/react/src/hooks/my-organization/__tests__/use-member-management-service.test.ts b/packages/react/src/hooks/my-organization/__tests__/use-member-management-service.test.ts
new file mode 100644
index 000000000..c007cd215
--- /dev/null
+++ b/packages/react/src/hooks/my-organization/__tests__/use-member-management-service.test.ts
@@ -0,0 +1,413 @@
+import { renderHook, waitFor, act } from '@testing-library/react';
+import { describe, it, expect, beforeEach, vi } from 'vitest';
+
+import { useMemberManagementService } from '@/hooks/my-organization/shared/services/use-member-management-service';
+import { memberManagementQueryKeys } from '@/hooks/my-organization/shared/services/use-member-management-service';
+import * as useCoreClientModule from '@/hooks/shared/use-core-client';
+import * as useTranslatorModule from '@/hooks/shared/use-translator';
+import { mockCore, mockToast, createMockI18nService } from '@/tests/utils';
+import { createMockInvitation } from '@/tests/utils/__mocks__/my-organization/member-management/invitation.mocks';
+import { createTestQueryClientWrapper } from '@/tests/utils/test-provider';
+import type { UseMemberManagementServiceOptions } from '@/types/my-organization/member-management/organization-member-management-types';
+
+const { mockedShowToast } = mockToast();
+const { initMockCoreClient } = mockCore();
+
+const createDefaultOptions = (
+ overrides?: Partial,
+): UseMemberManagementServiceOptions => ({
+ customMessages: {},
+ activeTab: 'invitations',
+ invitationParams: {
+ pageSize: 10,
+ fromToken: undefined,
+ sortConfig: { key: null, direction: 'asc' },
+ filters: {},
+ },
+ ...overrides,
+});
+
+const renderService = (options: UseMemberManagementServiceOptions) => {
+ const { wrapper, queryClient } = createTestQueryClientWrapper();
+ return {
+ queryClient,
+ ...renderHook(() => useMemberManagementService(options), { wrapper }),
+ };
+};
+
+describe('useMemberManagementService', () => {
+ let mockCoreClient: ReturnType;
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+
+ mockCoreClient = initMockCoreClient();
+
+ vi.spyOn(useCoreClientModule, 'useCoreClient').mockReturnValue({
+ coreClient: mockCoreClient,
+ });
+
+ vi.spyOn(useTranslatorModule, 'useTranslator').mockReturnValue({
+ t: createMockI18nService().translator('member_management'),
+ changeLanguage: vi.fn(),
+ currentLanguage: 'en',
+ fallbackLanguage: 'en',
+ });
+ });
+
+ describe('memberManagementQueryKeys', () => {
+ it('should have correct base key', () => {
+ expect(memberManagementQueryKeys.all).toEqual(['member-management']);
+ });
+
+ it('should have correct invitations key', () => {
+ expect(memberManagementQueryKeys.invitations()).toEqual(['member-management', 'invitations']);
+ });
+ });
+
+ describe('providersQuery', () => {
+ it('should fetch identity providers when invitations tab is active', async () => {
+ const options = createDefaultOptions({ activeTab: 'invitations' });
+ const { result } = renderService(options);
+
+ await waitFor(() => {
+ expect(result.current.providersQuery.isSuccess).toBe(true);
+ });
+
+ expect(
+ mockCoreClient.getMyOrganizationApiClient().organization.identityProviders.list,
+ ).toHaveBeenCalled();
+ });
+
+ it('should not fetch identity providers when members tab is active', () => {
+ const options = createDefaultOptions({ activeTab: 'members' });
+ const { result } = renderService(options);
+
+ expect(result.current.providersQuery.fetchStatus).toBe('idle');
+ });
+ });
+
+ describe('invitationsQuery', () => {
+ it('should fetch invitations when invitations tab is active', async () => {
+ const options = createDefaultOptions({ activeTab: 'invitations' });
+ const { result } = renderService(options);
+
+ await waitFor(() => {
+ expect(result.current.invitationsQuery.isSuccess).toBe(true);
+ });
+
+ expect(
+ mockCoreClient.getMyOrganizationApiClient().organization.invitations.list,
+ ).toHaveBeenCalledWith(
+ expect.objectContaining({
+ take: 10,
+ from: undefined,
+ sort: undefined,
+ }),
+ );
+ });
+
+ it('should not fetch invitations when members tab is active', () => {
+ const options = createDefaultOptions({ activeTab: 'members' });
+ const { result } = renderService(options);
+
+ expect(result.current.invitationsQuery.fetchStatus).toBe('idle');
+ });
+
+ it('should pass sort parameter when sort config has a valid key', async () => {
+ const options = createDefaultOptions({
+ invitationParams: {
+ pageSize: 10,
+ fromToken: undefined,
+ sortConfig: { key: 'created_at', direction: 'desc' },
+ filters: {},
+ },
+ });
+ const { result } = renderService(options);
+
+ await waitFor(() => {
+ expect(result.current.invitationsQuery.isSuccess).toBe(true);
+ });
+
+ expect(
+ mockCoreClient.getMyOrganizationApiClient().organization.invitations.list,
+ ).toHaveBeenCalledWith(
+ expect.objectContaining({
+ sort: 'created_at:-1',
+ }),
+ );
+ });
+
+ it('should pass fromToken when provided', async () => {
+ const options = createDefaultOptions({
+ invitationParams: {
+ pageSize: 10,
+ fromToken: 'token_abc',
+ sortConfig: { key: null, direction: 'asc' },
+ filters: {},
+ },
+ });
+ const { result } = renderService(options);
+
+ await waitFor(() => {
+ expect(result.current.invitationsQuery.isSuccess).toBe(true);
+ });
+
+ expect(
+ mockCoreClient.getMyOrganizationApiClient().organization.invitations.list,
+ ).toHaveBeenCalledWith(
+ expect.objectContaining({
+ from: 'token_abc',
+ }),
+ );
+ });
+
+ it('should return parsed invitations data', async () => {
+ const mockInvitation = createMockInvitation();
+ mockCoreClient.getMyOrganizationApiClient().organization.invitations.list = vi
+ .fn()
+ .mockResolvedValue({
+ data: [mockInvitation],
+ response: { next: 'next_token', total: 5 },
+ });
+
+ const options = createDefaultOptions();
+ const { result } = renderService(options);
+
+ await waitFor(() => {
+ expect(result.current.invitationsQuery.isSuccess).toBe(true);
+ });
+
+ expect(result.current.invitationsQuery.data).toEqual({
+ invitations: [mockInvitation],
+ next: 'next_token',
+ total: 5,
+ });
+ });
+ });
+
+ describe('createInvitationMutation', () => {
+ it('should create an invitation and show success toast', async () => {
+ const options = createDefaultOptions();
+ const { result } = renderService(options);
+
+ await act(async () => {
+ result.current.createInvitationMutation.mutate({
+ invitees: [{ email: 'new@example.com', roles: ['role_admin'] }],
+ });
+ });
+
+ await waitFor(() => {
+ expect(result.current.createInvitationMutation.isSuccess).toBe(true);
+ });
+
+ expect(
+ mockCoreClient.getMyOrganizationApiClient().organization.invitations.create,
+ ).toHaveBeenCalled();
+ expect(mockedShowToast).toHaveBeenCalledWith(expect.objectContaining({ type: 'success' }));
+ });
+
+ it('should call onBefore action and cancel if it returns false', async () => {
+ const onBefore = vi.fn().mockReturnValue(false);
+ const options = createDefaultOptions({
+ createInvitationAction: { onBefore },
+ });
+ const { result } = renderService(options);
+
+ await act(async () => {
+ result.current.createInvitationMutation.mutate({
+ invitees: [{ email: 'new@example.com' }],
+ });
+ });
+
+ await waitFor(() => {
+ expect(result.current.createInvitationMutation.isError).toBe(true);
+ });
+
+ expect(onBefore).toHaveBeenCalled();
+ expect(
+ mockCoreClient.getMyOrganizationApiClient().organization.invitations.create,
+ ).not.toHaveBeenCalled();
+ });
+
+ it('should call onAfter action on success', async () => {
+ const onAfter = vi.fn();
+ const options = createDefaultOptions({
+ createInvitationAction: { onAfter },
+ });
+ const { result } = renderService(options);
+
+ await act(async () => {
+ result.current.createInvitationMutation.mutate({
+ invitees: [{ email: 'new@example.com' }],
+ });
+ });
+
+ await waitFor(() => {
+ expect(result.current.createInvitationMutation.isSuccess).toBe(true);
+ });
+
+ expect(onAfter).toHaveBeenCalled();
+ });
+
+ it('should show error toast on failure', async () => {
+ mockCoreClient.getMyOrganizationApiClient().organization.invitations.create = vi
+ .fn()
+ .mockRejectedValue(new Error('Create failed'));
+
+ const options = createDefaultOptions();
+ const { result } = renderService(options);
+
+ await act(async () => {
+ result.current.createInvitationMutation.mutate({
+ invitees: [{ email: 'new@example.com' }],
+ });
+ });
+
+ await waitFor(() => {
+ expect(result.current.createInvitationMutation.isError).toBe(true);
+ });
+
+ expect(mockedShowToast).toHaveBeenCalledWith(expect.objectContaining({ type: 'error' }));
+ });
+ });
+
+ describe('revokeInvitationMutation', () => {
+ it('should revoke an invitation and show success toast', async () => {
+ const invitation = createMockInvitation();
+ const options = createDefaultOptions();
+ const { result } = renderService(options);
+
+ await act(async () => {
+ result.current.revokeInvitationMutation.mutate(invitation);
+ });
+
+ await waitFor(() => {
+ expect(result.current.revokeInvitationMutation.isSuccess).toBe(true);
+ });
+
+ expect(
+ mockCoreClient.getMyOrganizationApiClient().organization.invitations.delete,
+ ).toHaveBeenCalledWith(invitation.id);
+ expect(mockedShowToast).toHaveBeenCalledWith(expect.objectContaining({ type: 'success' }));
+ });
+
+ it('should call onBefore action and cancel if it returns false', async () => {
+ const onBefore = vi.fn().mockReturnValue(false);
+ const invitation = createMockInvitation();
+ const options = createDefaultOptions({
+ revokeInvitationAction: { onBefore },
+ });
+ const { result } = renderService(options);
+
+ await act(async () => {
+ result.current.revokeInvitationMutation.mutate(invitation);
+ });
+
+ await waitFor(() => {
+ expect(result.current.revokeInvitationMutation.isError).toBe(true);
+ });
+
+ expect(onBefore).toHaveBeenCalledWith(invitation);
+ expect(
+ mockCoreClient.getMyOrganizationApiClient().organization.invitations.delete,
+ ).not.toHaveBeenCalled();
+ });
+
+ it('should show error toast on failure', async () => {
+ mockCoreClient.getMyOrganizationApiClient().organization.invitations.delete = vi
+ .fn()
+ .mockRejectedValue(new Error('Revoke failed'));
+
+ const invitation = createMockInvitation();
+ const options = createDefaultOptions();
+ const { result } = renderService(options);
+
+ await act(async () => {
+ result.current.revokeInvitationMutation.mutate(invitation);
+ });
+
+ await waitFor(() => {
+ expect(result.current.revokeInvitationMutation.isError).toBe(true);
+ });
+
+ expect(mockedShowToast).toHaveBeenCalledWith(expect.objectContaining({ type: 'error' }));
+ });
+ });
+
+ describe('resendInvitationMutation', () => {
+ it('should revoke and resend an invitation', async () => {
+ const invitation = createMockInvitation();
+ const options = createDefaultOptions();
+ const { result } = renderService(options);
+
+ await act(async () => {
+ result.current.resendInvitationMutation.mutate(invitation);
+ });
+
+ await waitFor(() => {
+ expect(result.current.resendInvitationMutation.isSuccess).toBe(true);
+ });
+
+ const orgApi = mockCoreClient.getMyOrganizationApiClient().organization;
+ expect(orgApi.invitations.get).toHaveBeenCalledWith(invitation.id);
+ expect(orgApi.invitations.delete).toHaveBeenCalled();
+ expect(orgApi.invitations.create).toHaveBeenCalled();
+ expect(mockedShowToast).toHaveBeenCalledWith(expect.objectContaining({ type: 'success' }));
+ });
+
+ it('should call onBefore action and cancel if it returns false', async () => {
+ const onBefore = vi.fn().mockReturnValue(false);
+ const invitation = createMockInvitation();
+ const options = createDefaultOptions({
+ resendInvitationAction: { onBefore },
+ });
+ const { result } = renderService(options);
+
+ await act(async () => {
+ result.current.resendInvitationMutation.mutate(invitation);
+ });
+
+ await waitFor(() => {
+ expect(result.current.resendInvitationMutation.isError).toBe(true);
+ });
+
+ expect(onBefore).toHaveBeenCalledWith(invitation);
+ });
+
+ it('should show error toast on failure', async () => {
+ mockCoreClient.getMyOrganizationApiClient().organization.invitations.get = vi
+ .fn()
+ .mockRejectedValue(new Error('Fetch failed'));
+
+ const invitation = createMockInvitation();
+ const options = createDefaultOptions();
+ const { result } = renderService(options);
+
+ await act(async () => {
+ result.current.resendInvitationMutation.mutate(invitation);
+ });
+
+ await waitFor(() => {
+ expect(result.current.resendInvitationMutation.isError).toBe(true);
+ });
+
+ expect(mockedShowToast).toHaveBeenCalledWith(expect.objectContaining({ type: 'error' }));
+ });
+ });
+
+ describe('fetchInvitationDetails', () => {
+ it('should fetch invitation details by id', async () => {
+ const mockInvitation = createMockInvitation();
+ const options = createDefaultOptions();
+ const { result } = renderService(options);
+
+ const details = await result.current.fetchInvitationDetails('inv_abc123xyz456');
+
+ expect(
+ mockCoreClient.getMyOrganizationApiClient().organization.invitations.get,
+ ).toHaveBeenCalledWith('inv_abc123xyz456');
+ expect(details).toEqual(mockInvitation);
+ });
+ });
+});
diff --git a/packages/react/src/tests/utils/__mocks__/core/core-client.mocks.ts b/packages/react/src/tests/utils/__mocks__/core/core-client.mocks.ts
index 2964c44ac..02aa5053c 100644
--- a/packages/react/src/tests/utils/__mocks__/core/core-client.mocks.ts
+++ b/packages/react/src/tests/utils/__mocks__/core/core-client.mocks.ts
@@ -8,6 +8,7 @@ import {
createMockEmptyAuthenticationMethods,
} from '@/tests/utils/__mocks__/my-account/mfa/mfa.mocks';
import { createMockIdentityProvider } from '@/tests/utils/__mocks__/my-organization/domain-management/domain.mocks';
+import { createMockInvitation } from '@/tests/utils/__mocks__/my-organization/member-management/invitation.mocks';
import { createMockOrganization } from '@/tests/utils/__mocks__/my-organization/organization-management/organization-details.mocks';
const createMockMyAccountApiService = (): CoreClientInterface['myAccountApiClient'] => {
@@ -55,6 +56,15 @@ const createMockMyOrgApiService = (): CoreClientInterface['myOrganizationApiClie
delete: vi.fn().mockResolvedValue(undefined),
},
},
+ invitations: {
+ list: vi.fn().mockResolvedValue({
+ data: [createMockInvitation()],
+ response: { next: null },
+ }),
+ get: vi.fn().mockResolvedValue(createMockInvitation()),
+ create: vi.fn().mockResolvedValue([createMockInvitation()]),
+ delete: vi.fn().mockResolvedValue(undefined),
+ },
domains: {
list: vi.fn().mockResolvedValue([]),
create: vi.fn().mockResolvedValue({}),
diff --git a/packages/react/src/tests/utils/__mocks__/my-organization/member-management/invitation.mocks.ts b/packages/react/src/tests/utils/__mocks__/my-organization/member-management/invitation.mocks.ts
new file mode 100644
index 000000000..79ec5d15d
--- /dev/null
+++ b/packages/react/src/tests/utils/__mocks__/my-organization/member-management/invitation.mocks.ts
@@ -0,0 +1,108 @@
+import type { MemberInvitation } from '@auth0/universal-components-core';
+import { vi } from 'vitest';
+
+import type { OrganizationInvitationDetailsModalProps } from '@/components/auth0/my-organization/shared/member-management/invitations/invitation-details/organization-invitation-details-modal';
+import type { OrganizationInvitationRevokeModalProps } from '@/components/auth0/my-organization/shared/member-management/invitations/invitation-revoke/organization-invitation-revoke-modal';
+import type { OrganizationInvitationCreateModalProps } from '@/components/auth0/my-organization/shared/member-management/shared/invitation-create/organization-invitation-create-modal';
+import type {
+ RoleOption,
+ IdentityProviderOption,
+ OrganizationInvitationTableActionsColumnProps,
+ SearchFilterProps,
+} from '@/types/my-organization/member-management/organization-invitation-table-types';
+
+export const createMockInvitation = (overrides?: Partial): MemberInvitation => ({
+ id: 'inv_abc123xyz456',
+ invitee: { email: 'test@example.com' },
+ inviter: { name: 'Admin User' },
+ roles: ['role_admin'],
+ created_at: '2024-01-01T00:00:00.000Z',
+ expires_at: '2099-12-31T23:59:59.000Z',
+ invitation_url: 'https://example.auth0.com/invitation?ticket=abc123',
+ ...overrides,
+});
+
+export const createMockPendingInvitation = (
+ overrides?: Partial,
+): MemberInvitation =>
+ createMockInvitation({
+ invitation_url: 'https://example.auth0.com/invitation?ticket=pending123',
+ ...overrides,
+ });
+
+export const createMockExpiredInvitation = (
+ overrides?: Partial,
+): MemberInvitation =>
+ createMockInvitation({
+ expires_at: '2020-01-01T00:00:00.000Z',
+ invitation_url: undefined,
+ ...overrides,
+ });
+
+export const createMockRoles = (): RoleOption[] => [
+ { id: 'role_admin', name: 'Admin', description: 'Administrator role' },
+ { id: 'role_member', name: 'Member', description: 'Member role' },
+ { id: 'role_viewer', name: 'Viewer', description: 'Viewer role' },
+];
+
+export const createMockProviders = (): IdentityProviderOption[] => [
+ { id: 'con_provider1', name: 'Google', type: 'social' },
+ { id: 'con_provider2', name: 'Okta', type: 'enterprise' },
+];
+
+export const createMockCreateModalProps = (
+ overrides: Partial = {},
+): OrganizationInvitationCreateModalProps => ({
+ isOpen: true,
+ isLoading: false,
+ onClose: vi.fn(),
+ onCreate: vi.fn(),
+ ...overrides,
+});
+
+export const createMockActionsColumnProps = (
+ overrides: Partial = {},
+): OrganizationInvitationTableActionsColumnProps => ({
+ invitation: createMockPendingInvitation(),
+ readOnly: false,
+ onViewDetails: vi.fn(),
+ onCopyUrl: vi.fn(),
+ onRevokeAndResend: vi.fn(),
+ onRevoke: vi.fn(),
+ ...overrides,
+});
+
+export const createMockDetailsModalProps = (
+ overrides: Partial = {},
+): OrganizationInvitationDetailsModalProps => ({
+ invitation: createMockPendingInvitation(),
+ isOpen: true,
+ isRevoking: false,
+ isResending: false,
+ onClose: vi.fn(),
+ onCopyUrl: vi.fn(),
+ onRevoke: vi.fn(),
+ onResend: vi.fn(),
+ ...overrides,
+});
+
+export const createMockRevokeModalProps = (
+ overrides: Partial = {},
+): OrganizationInvitationRevokeModalProps => ({
+ invitation: createMockPendingInvitation(),
+ isOpen: true,
+ isLoading: false,
+ isRevokeAndResend: false,
+ onClose: vi.fn(),
+ onConfirm: vi.fn(),
+ ...overrides,
+});
+
+export const createMockSearchFilterProps = (
+ overrides: Partial = {},
+): SearchFilterProps => ({
+ filters: {},
+ availableRoles: createMockRoles(),
+ onRoleFilterChange: vi.fn(),
+ ...overrides,
+});