feat: Add comprehensive test suite and misc improvements

- Add frontend unit tests with Vitest for components, hooks, pages, and utilities
- Add backend tests for webhooks, notifications, middleware, and edge cases
- Add ForgotPassword, NotFound, and ResetPassword pages
- Add migration for orphaned staff resources conversion
- Add coverage directory to gitignore (generated reports)
- Various bug fixes and improvements from previous work

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
poduck
2025-12-08 02:36:46 -05:00
parent c220612214
commit 8dc2248f1f
145 changed files with 77947 additions and 1048 deletions

View File

@@ -0,0 +1,902 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { renderHook, waitFor, act } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import React from 'react';
// Mock apiClient
vi.mock('../../api/client', () => ({
default: {
get: vi.fn(),
post: vi.fn(),
delete: vi.fn(),
},
}));
import {
useInvitations,
useCreateInvitation,
useCancelInvitation,
useResendInvitation,
useInvitationDetails,
useAcceptInvitation,
useDeclineInvitation,
StaffInvitation,
InvitationDetails,
CreateInvitationData,
} from '../useInvitations';
import apiClient from '../../api/client';
// Create wrapper
const createWrapper = () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});
return function Wrapper({ children }: { children: React.ReactNode }) {
return React.createElement(QueryClientProvider, { client: queryClient }, children);
};
};
describe('useInvitations hooks', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('useInvitations', () => {
it('fetches pending invitations successfully', async () => {
const mockInvitations: StaffInvitation[] = [
{
id: 1,
email: 'john@example.com',
role: 'TENANT_MANAGER',
role_display: 'Manager',
status: 'PENDING',
invited_by: 5,
invited_by_name: 'Admin User',
created_at: '2024-01-01T10:00:00Z',
expires_at: '2024-01-08T10:00:00Z',
accepted_at: null,
create_bookable_resource: false,
resource_name: '',
permissions: { can_invite_staff: true },
},
{
id: 2,
email: 'jane@example.com',
role: 'TENANT_STAFF',
role_display: 'Staff',
status: 'PENDING',
invited_by: 5,
invited_by_name: 'Admin User',
created_at: '2024-01-02T10:00:00Z',
expires_at: '2024-01-09T10:00:00Z',
accepted_at: null,
create_bookable_resource: true,
resource_name: 'Jane',
permissions: {},
},
];
vi.mocked(apiClient.get).mockResolvedValue({ data: mockInvitations });
const { result } = renderHook(() => useInvitations(), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(apiClient.get).toHaveBeenCalledWith('/staff/invitations/');
expect(result.current.data).toEqual(mockInvitations);
expect(result.current.data).toHaveLength(2);
});
it('returns empty array when no invitations exist', async () => {
vi.mocked(apiClient.get).mockResolvedValue({ data: [] });
const { result } = renderHook(() => useInvitations(), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(result.current.data).toEqual([]);
});
it('handles API errors gracefully', async () => {
vi.mocked(apiClient.get).mockRejectedValue(new Error('Network error'));
const { result } = renderHook(() => useInvitations(), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isError).toBe(true);
});
expect(result.current.data).toBeUndefined();
});
it('uses correct query key for cache management', async () => {
vi.mocked(apiClient.get).mockResolvedValue({ data: [] });
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
},
});
const wrapper = ({ children }: { children: React.ReactNode }) =>
React.createElement(QueryClientProvider, { client: queryClient }, children);
renderHook(() => useInvitations(), { wrapper });
await waitFor(() => {
const cache = queryClient.getQueryCache();
const queries = cache.findAll({ queryKey: ['invitations'] });
expect(queries.length).toBe(1);
});
});
});
describe('useCreateInvitation', () => {
it('creates invitation with minimal data', async () => {
const invitationData: CreateInvitationData = {
email: 'new@example.com',
role: 'TENANT_STAFF',
};
const mockResponse = {
id: 3,
email: 'new@example.com',
role: 'TENANT_STAFF',
status: 'PENDING',
};
vi.mocked(apiClient.post).mockResolvedValue({ data: mockResponse });
const { result } = renderHook(() => useCreateInvitation(), {
wrapper: createWrapper(),
});
await act(async () => {
await result.current.mutateAsync(invitationData);
});
expect(apiClient.post).toHaveBeenCalledWith('/staff/invitations/', invitationData);
});
it('creates invitation with full data including resource', async () => {
const invitationData: CreateInvitationData = {
email: 'staff@example.com',
role: 'TENANT_STAFF',
create_bookable_resource: true,
resource_name: 'New Staff Member',
permissions: {
can_view_all_schedules: true,
can_manage_own_appointments: true,
},
};
const mockResponse = {
id: 4,
email: 'staff@example.com',
role: 'TENANT_STAFF',
create_bookable_resource: true,
resource_name: 'New Staff Member',
};
vi.mocked(apiClient.post).mockResolvedValue({ data: mockResponse });
const { result } = renderHook(() => useCreateInvitation(), {
wrapper: createWrapper(),
});
await act(async () => {
await result.current.mutateAsync(invitationData);
});
expect(apiClient.post).toHaveBeenCalledWith('/staff/invitations/', invitationData);
});
it('creates manager invitation with permissions', async () => {
const invitationData: CreateInvitationData = {
email: 'manager@example.com',
role: 'TENANT_MANAGER',
permissions: {
can_invite_staff: true,
can_manage_resources: true,
can_manage_services: true,
can_view_reports: true,
can_access_settings: false,
can_refund_payments: false,
},
};
const mockResponse = { id: 5, email: 'manager@example.com', role: 'TENANT_MANAGER' };
vi.mocked(apiClient.post).mockResolvedValue({ data: mockResponse });
const { result } = renderHook(() => useCreateInvitation(), {
wrapper: createWrapper(),
});
await act(async () => {
await result.current.mutateAsync(invitationData);
});
expect(apiClient.post).toHaveBeenCalledWith('/staff/invitations/', invitationData);
});
it('invalidates invitations query on success', async () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});
const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries');
vi.mocked(apiClient.post).mockResolvedValue({ data: { id: 1 } });
const wrapper = ({ children }: { children: React.ReactNode }) =>
React.createElement(QueryClientProvider, { client: queryClient }, children);
const { result } = renderHook(() => useCreateInvitation(), { wrapper });
await act(async () => {
await result.current.mutateAsync({
email: 'test@example.com',
role: 'TENANT_STAFF',
});
});
expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['invitations'] });
});
it('handles API errors during creation', async () => {
const errorMessage = 'Email already invited';
vi.mocked(apiClient.post).mockRejectedValue(new Error(errorMessage));
const { result } = renderHook(() => useCreateInvitation(), {
wrapper: createWrapper(),
});
let caughtError: Error | null = null;
await act(async () => {
try {
await result.current.mutateAsync({
email: 'duplicate@example.com',
role: 'TENANT_STAFF',
});
} catch (error) {
caughtError = error as Error;
}
});
expect(caughtError).toBeInstanceOf(Error);
expect(caughtError?.message).toBe(errorMessage);
});
it('returns created invitation data', async () => {
const mockResponse = {
id: 10,
email: 'created@example.com',
role: 'TENANT_STAFF',
status: 'PENDING',
};
vi.mocked(apiClient.post).mockResolvedValue({ data: mockResponse });
const { result } = renderHook(() => useCreateInvitation(), {
wrapper: createWrapper(),
});
let responseData;
await act(async () => {
responseData = await result.current.mutateAsync({
email: 'created@example.com',
role: 'TENANT_STAFF',
});
});
expect(responseData).toEqual(mockResponse);
});
});
describe('useCancelInvitation', () => {
it('cancels invitation by id', async () => {
vi.mocked(apiClient.delete).mockResolvedValue({ data: undefined });
const { result } = renderHook(() => useCancelInvitation(), {
wrapper: createWrapper(),
});
await act(async () => {
await result.current.mutateAsync(1);
});
expect(apiClient.delete).toHaveBeenCalledWith('/staff/invitations/1/');
});
it('invalidates invitations query on success', async () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});
const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries');
vi.mocked(apiClient.delete).mockResolvedValue({ data: undefined });
const wrapper = ({ children }: { children: React.ReactNode }) =>
React.createElement(QueryClientProvider, { client: queryClient }, children);
const { result } = renderHook(() => useCancelInvitation(), { wrapper });
await act(async () => {
await result.current.mutateAsync(5);
});
expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['invitations'] });
});
it('handles API errors during cancellation', async () => {
const errorMessage = 'Invitation not found';
vi.mocked(apiClient.delete).mockRejectedValue(new Error(errorMessage));
const { result } = renderHook(() => useCancelInvitation(), {
wrapper: createWrapper(),
});
let caughtError: Error | null = null;
await act(async () => {
try {
await result.current.mutateAsync(999);
} catch (error) {
caughtError = error as Error;
}
});
expect(caughtError).toBeInstanceOf(Error);
expect(caughtError?.message).toBe(errorMessage);
});
});
describe('useResendInvitation', () => {
it('resends invitation email', async () => {
const mockResponse = { message: 'Invitation email sent' };
vi.mocked(apiClient.post).mockResolvedValue({ data: mockResponse });
const { result } = renderHook(() => useResendInvitation(), {
wrapper: createWrapper(),
});
await act(async () => {
await result.current.mutateAsync(2);
});
expect(apiClient.post).toHaveBeenCalledWith('/staff/invitations/2/resend/');
});
it('returns response data', async () => {
const mockResponse = { message: 'Email resent successfully', sent_at: '2024-01-01T12:00:00Z' };
vi.mocked(apiClient.post).mockResolvedValue({ data: mockResponse });
const { result } = renderHook(() => useResendInvitation(), {
wrapper: createWrapper(),
});
let responseData;
await act(async () => {
responseData = await result.current.mutateAsync(3);
});
expect(responseData).toEqual(mockResponse);
});
it('handles API errors during resend', async () => {
const errorMessage = 'Invitation already accepted';
vi.mocked(apiClient.post).mockRejectedValue(new Error(errorMessage));
const { result } = renderHook(() => useResendInvitation(), {
wrapper: createWrapper(),
});
let caughtError: Error | null = null;
await act(async () => {
try {
await result.current.mutateAsync(10);
} catch (error) {
caughtError = error as Error;
}
});
expect(caughtError).toBeInstanceOf(Error);
expect(caughtError?.message).toBe(errorMessage);
});
it('does not invalidate queries (resend does not modify invitation list)', async () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});
const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries');
vi.mocked(apiClient.post).mockResolvedValue({ data: {} });
const wrapper = ({ children }: { children: React.ReactNode }) =>
React.createElement(QueryClientProvider, { client: queryClient }, children);
const { result } = renderHook(() => useResendInvitation(), { wrapper });
await act(async () => {
await result.current.mutateAsync(1);
});
// Should not invalidate invitations query
expect(invalidateSpy).not.toHaveBeenCalled();
});
});
describe('useInvitationDetails', () => {
it('fetches platform tenant invitation first and returns with tenant type', async () => {
const mockPlatformInvitation: Omit<InvitationDetails, 'invitation_type'> = {
email: 'tenant@example.com',
role: 'OWNER',
role_display: 'Business Owner',
business_name: 'New Business',
invited_by: 'Platform Admin',
expires_at: '2024-01-15T10:00:00Z',
};
vi.mocked(apiClient.get).mockResolvedValue({ data: mockPlatformInvitation });
const { result } = renderHook(() => useInvitationDetails('valid-token-123'), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(apiClient.get).toHaveBeenCalledWith('/platform/tenant-invitations/token/valid-token-123/');
expect(result.current.data).toEqual({
...mockPlatformInvitation,
invitation_type: 'tenant',
});
});
it('falls back to staff invitation when platform request fails', async () => {
const mockStaffInvitation: Omit<InvitationDetails, 'invitation_type'> = {
email: 'staff@example.com',
role: 'TENANT_STAFF',
role_display: 'Staff',
business_name: 'Existing Business',
invited_by: 'Manager',
expires_at: '2024-01-15T10:00:00Z',
create_bookable_resource: true,
resource_name: 'Staff Member',
};
// First call fails (platform), second succeeds (staff)
vi.mocked(apiClient.get)
.mockRejectedValueOnce(new Error('Not found'))
.mockResolvedValueOnce({ data: mockStaffInvitation });
const { result } = renderHook(() => useInvitationDetails('staff-token-456'), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(apiClient.get).toHaveBeenCalledWith('/platform/tenant-invitations/token/staff-token-456/');
expect(apiClient.get).toHaveBeenCalledWith('/staff/invitations/token/staff-token-456/');
expect(result.current.data).toEqual({
...mockStaffInvitation,
invitation_type: 'staff',
});
});
it('returns error when both platform and staff requests fail', async () => {
vi.mocked(apiClient.get)
.mockRejectedValueOnce(new Error('Platform not found'))
.mockRejectedValueOnce(new Error('Staff not found'));
const { result } = renderHook(() => useInvitationDetails('invalid-token'), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isError).toBe(true);
});
expect(result.current.data).toBeUndefined();
});
it('does not fetch when token is null', async () => {
const { result } = renderHook(() => useInvitationDetails(null), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});
expect(apiClient.get).not.toHaveBeenCalled();
expect(result.current.data).toBeUndefined();
});
it('does not fetch when token is empty string', async () => {
const { result } = renderHook(() => useInvitationDetails(''), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});
expect(apiClient.get).not.toHaveBeenCalled();
});
it('does not retry on failure', async () => {
vi.mocked(apiClient.get)
.mockRejectedValueOnce(new Error('Platform error'))
.mockRejectedValueOnce(new Error('Staff error'));
const { result } = renderHook(() => useInvitationDetails('token'), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isError).toBe(true);
});
// Called twice total: once for platform, once for staff (no retries)
expect(apiClient.get).toHaveBeenCalledTimes(2);
});
});
describe('useAcceptInvitation', () => {
const acceptPayload = {
token: 'test-token',
firstName: 'John',
lastName: 'Doe',
password: 'SecurePass123!',
};
it('accepts staff invitation when invitationType is staff', async () => {
const mockResponse = { message: 'Invitation accepted', user_id: 1 };
vi.mocked(apiClient.post).mockResolvedValue({ data: mockResponse });
const { result } = renderHook(() => useAcceptInvitation(), {
wrapper: createWrapper(),
});
await act(async () => {
await result.current.mutateAsync({
...acceptPayload,
invitationType: 'staff',
});
});
expect(apiClient.post).toHaveBeenCalledWith('/staff/invitations/token/test-token/accept/', {
first_name: 'John',
last_name: 'Doe',
password: 'SecurePass123!',
});
expect(apiClient.post).toHaveBeenCalledTimes(1);
});
it('tries platform tenant invitation first when invitationType not provided', async () => {
const mockResponse = { message: 'Tenant invitation accepted', business_id: 5 };
vi.mocked(apiClient.post).mockResolvedValue({ data: mockResponse });
const { result } = renderHook(() => useAcceptInvitation(), {
wrapper: createWrapper(),
});
await act(async () => {
await result.current.mutateAsync(acceptPayload);
});
expect(apiClient.post).toHaveBeenCalledWith('/platform/tenant-invitations/token/test-token/accept/', {
first_name: 'John',
last_name: 'Doe',
password: 'SecurePass123!',
});
});
it('tries platform tenant invitation first when invitationType is tenant', async () => {
const mockResponse = { message: 'Tenant invitation accepted' };
vi.mocked(apiClient.post).mockResolvedValue({ data: mockResponse });
const { result } = renderHook(() => useAcceptInvitation(), {
wrapper: createWrapper(),
});
await act(async () => {
await result.current.mutateAsync({
...acceptPayload,
invitationType: 'tenant',
});
});
expect(apiClient.post).toHaveBeenCalledWith('/platform/tenant-invitations/token/test-token/accept/', {
first_name: 'John',
last_name: 'Doe',
password: 'SecurePass123!',
});
});
it('falls back to staff invitation when platform request fails', async () => {
const mockResponse = { message: 'Staff invitation accepted' };
vi.mocked(apiClient.post)
.mockRejectedValueOnce(new Error('Platform not found'))
.mockResolvedValueOnce({ data: mockResponse });
const { result } = renderHook(() => useAcceptInvitation(), {
wrapper: createWrapper(),
});
await act(async () => {
await result.current.mutateAsync(acceptPayload);
});
expect(apiClient.post).toHaveBeenCalledWith('/platform/tenant-invitations/token/test-token/accept/', {
first_name: 'John',
last_name: 'Doe',
password: 'SecurePass123!',
});
expect(apiClient.post).toHaveBeenCalledWith('/staff/invitations/token/test-token/accept/', {
first_name: 'John',
last_name: 'Doe',
password: 'SecurePass123!',
});
expect(apiClient.post).toHaveBeenCalledTimes(2);
});
it('throws error when both platform and staff requests fail', async () => {
vi.mocked(apiClient.post)
.mockRejectedValueOnce(new Error('Platform error'))
.mockRejectedValueOnce(new Error('Staff error'));
const { result } = renderHook(() => useAcceptInvitation(), {
wrapper: createWrapper(),
});
let caughtError: Error | null = null;
await act(async () => {
try {
await result.current.mutateAsync(acceptPayload);
} catch (error) {
caughtError = error as Error;
}
});
expect(caughtError).toBeInstanceOf(Error);
expect(caughtError?.message).toBe('Staff error');
});
it('returns response data on successful acceptance', async () => {
const mockResponse = {
message: 'Success',
user: { id: 1, email: 'john@example.com' },
};
vi.mocked(apiClient.post).mockResolvedValue({ data: mockResponse });
const { result } = renderHook(() => useAcceptInvitation(), {
wrapper: createWrapper(),
});
let responseData;
await act(async () => {
responseData = await result.current.mutateAsync({
...acceptPayload,
invitationType: 'staff',
});
});
expect(responseData).toEqual(mockResponse);
});
});
describe('useDeclineInvitation', () => {
it('declines staff invitation', async () => {
const mockResponse = { message: 'Invitation declined' };
vi.mocked(apiClient.post).mockResolvedValue({ data: mockResponse });
const { result } = renderHook(() => useDeclineInvitation(), {
wrapper: createWrapper(),
});
await act(async () => {
await result.current.mutateAsync({
token: 'staff-token',
invitationType: 'staff',
});
});
expect(apiClient.post).toHaveBeenCalledWith('/staff/invitations/token/staff-token/decline/');
});
it('attempts to decline tenant invitation', async () => {
const mockResponse = { message: 'Tenant invitation declined' };
vi.mocked(apiClient.post).mockResolvedValue({ data: mockResponse });
const { result } = renderHook(() => useDeclineInvitation(), {
wrapper: createWrapper(),
});
await act(async () => {
await result.current.mutateAsync({
token: 'tenant-token',
invitationType: 'tenant',
});
});
expect(apiClient.post).toHaveBeenCalledWith('/platform/tenant-invitations/token/tenant-token/decline/');
});
it('returns success status when tenant decline endpoint does not exist', async () => {
vi.mocked(apiClient.post).mockRejectedValue(new Error('Not found'));
const { result } = renderHook(() => useDeclineInvitation(), {
wrapper: createWrapper(),
});
let responseData;
await act(async () => {
responseData = await result.current.mutateAsync({
token: 'tenant-token',
invitationType: 'tenant',
});
});
expect(responseData).toEqual({ status: 'declined' });
});
it('declines staff invitation when invitationType not provided', async () => {
const mockResponse = { message: 'Staff invitation declined' };
vi.mocked(apiClient.post).mockResolvedValue({ data: mockResponse });
const { result } = renderHook(() => useDeclineInvitation(), {
wrapper: createWrapper(),
});
await act(async () => {
await result.current.mutateAsync({
token: 'default-token',
});
});
expect(apiClient.post).toHaveBeenCalledWith('/staff/invitations/token/default-token/decline/');
});
it('handles API errors for staff invitation decline', async () => {
const errorMessage = 'Invitation already processed';
vi.mocked(apiClient.post).mockRejectedValue(new Error(errorMessage));
const { result } = renderHook(() => useDeclineInvitation(), {
wrapper: createWrapper(),
});
let caughtError: Error | null = null;
await act(async () => {
try {
await result.current.mutateAsync({
token: 'invalid-token',
invitationType: 'staff',
});
} catch (error) {
caughtError = error as Error;
}
});
expect(caughtError).toBeInstanceOf(Error);
expect(caughtError?.message).toBe(errorMessage);
});
it('returns response data on successful decline', async () => {
const mockResponse = {
message: 'Successfully declined',
invitation_id: 5,
};
vi.mocked(apiClient.post).mockResolvedValue({ data: mockResponse });
const { result } = renderHook(() => useDeclineInvitation(), {
wrapper: createWrapper(),
});
let responseData;
await act(async () => {
responseData = await result.current.mutateAsync({
token: 'token',
invitationType: 'staff',
});
});
expect(responseData).toEqual(mockResponse);
});
});
describe('Edge cases and integration scenarios', () => {
it('handles multiple invitation operations in sequence', async () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});
const wrapper = ({ children }: { children: React.ReactNode }) =>
React.createElement(QueryClientProvider, { client: queryClient }, children);
// Mock responses
vi.mocked(apiClient.get).mockResolvedValue({ data: [] });
vi.mocked(apiClient.post).mockResolvedValue({ data: { id: 1 } });
vi.mocked(apiClient.delete).mockResolvedValue({ data: undefined });
const { result: createResult } = renderHook(() => useCreateInvitation(), { wrapper });
const { result: listResult } = renderHook(() => useInvitations(), { wrapper });
const { result: cancelResult } = renderHook(() => useCancelInvitation(), { wrapper });
// Create invitation
await act(async () => {
await createResult.current.mutateAsync({
email: 'test@example.com',
role: 'TENANT_STAFF',
});
});
// Cancel invitation
await act(async () => {
await cancelResult.current.mutateAsync(1);
});
// Verify list is called
await waitFor(() => {
expect(listResult.current.isSuccess).toBe(true);
});
expect(apiClient.post).toHaveBeenCalled();
expect(apiClient.delete).toHaveBeenCalled();
});
it('handles concurrent invitation details fetching with different tokens', async () => {
const platformData = { email: 'platform@example.com', business_name: 'Platform Biz' };
const staffData = { email: 'staff@example.com', business_name: 'Staff Biz' };
vi.mocked(apiClient.get)
.mockResolvedValueOnce({ data: platformData })
.mockRejectedValueOnce(new Error('Not found'))
.mockResolvedValueOnce({ data: staffData });
const { result: result1 } = renderHook(() => useInvitationDetails('token1'), {
wrapper: createWrapper(),
});
const { result: result2 } = renderHook(() => useInvitationDetails('token2'), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result1.current.isSuccess).toBe(true);
expect(result2.current.isSuccess).toBe(true);
});
expect(result1.current.data?.invitation_type).toBe('tenant');
expect(result2.current.data?.invitation_type).toBe('staff');
});
});
});