import { renderHook, waitFor, act } from '@testing-library/react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { http, HttpResponse } from 'msw'; import { createElement } from 'react'; import { describe, expect, it } from 'vitest'; import { server } from '@/mocks/server'; import { API_BASE } from '@/features/auth/mocks/fixtures'; import { getMockUsers } from '@/features/users/mocks/handlers'; import { useUsers, useInviteUser, useChangeRole, useSetUserActive, useDeleteUser } from './useUsers'; function createWrapper() { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false } }, }); return ({ children }: { children: React.ReactNode }) => createElement(QueryClientProvider, { client: queryClient }, children); } const USERS_URL = `${API_BASE}/api/v1/Users`; describe('useUsers', () => { it('starts in loading state', () => { const { result } = renderHook(() => useUsers(), { wrapper: createWrapper() }); expect(result.current.isPending).toBe(true); }); it('returns the users list on success', async () => { const { result } = renderHook(() => useUsers(), { wrapper: createWrapper() }); await waitFor(() => expect(result.current.isPending).toBe(false)); expect(result.current.isError).toBe(false); expect(result.current.data).toHaveLength(getMockUsers().length); expect(result.current.data?.[0].email).toBe(getMockUsers()[0].email); }); it('enters error state on 5xx', async () => { server.use(http.get(USERS_URL, () => HttpResponse.json({}, { status: 500 }))); const { result } = renderHook(() => useUsers(), { wrapper: createWrapper() }); await waitFor(() => expect(result.current.isPending).toBe(false)); expect(result.current.isError).toBe(true); }); }); describe('useInviteUser', () => { it('returns inviteLink on successful mutation', async () => { const { result } = renderHook(() => useInviteUser(), { wrapper: createWrapper() }); await act(async () => { await result.current.mutateAsync({ email: 'new@example.com', role: 'User' }); }); await waitFor(() => expect(result.current.isSuccess).toBe(true)); expect(result.current.data?.inviteLink).toContain('/invite/complete'); }); it('fails when server returns 400', async () => { server.use( http.post(`${USERS_URL}/invite`, () => HttpResponse.json({ detail: 'already exists' }, { status: 400 }), ), ); const { result } = renderHook(() => useInviteUser(), { wrapper: createWrapper() }); await act(async () => { await result.current.mutateAsync({ email: 'dup@example.com', role: 'User' }).catch(() => {}); }); await waitFor(() => expect(result.current.isError).toBe(true)); }); }); describe('useChangeRole', () => { it('succeeds when server returns 200', async () => { const { result } = renderHook(() => useChangeRole(), { wrapper: createWrapper() }); await act(async () => { await result.current.mutateAsync({ userId: '22222222-2222-2222-2222-222222222222', newRole: 'User', }); }); await waitFor(() => expect(result.current.isSuccess).toBe(true)); }); it('fails when server returns 403', async () => { server.use( http.put(`${USERS_URL}/:userId/role`, () => new HttpResponse(null, { status: 403 })), ); const { result } = renderHook(() => useChangeRole(), { wrapper: createWrapper() }); await act(async () => { await result.current .mutateAsync({ userId: 'any-id', newRole: 'Administrator' }) .catch(() => {}); }); await waitFor(() => expect(result.current.isError).toBe(true)); }); }); describe('useSetUserActive', () => { it('succeeds when server returns 200', async () => { const { result } = renderHook(() => useSetUserActive(), { wrapper: createWrapper() }); await act(async () => { await result.current.mutateAsync({ userId: '22222222-2222-2222-2222-222222222222', isActive: false, }); }); await waitFor(() => expect(result.current.isSuccess).toBe(true)); }); it('fails when server returns 400', async () => { server.use( http.put(`${USERS_URL}/:userId/active`, () => new HttpResponse(null, { status: 400 })), ); const { result } = renderHook(() => useSetUserActive(), { wrapper: createWrapper() }); await act(async () => { await result.current .mutateAsync({ userId: 'any-id', isActive: false }) .catch(() => {}); }); await waitFor(() => expect(result.current.isError).toBe(true)); }); }); describe('useDeleteUser', () => { it('succeeds when server returns 204', async () => { const { result } = renderHook(() => useDeleteUser(), { wrapper: createWrapper() }); await act(async () => { await result.current.mutateAsync('22222222-2222-2222-2222-222222222222'); }); await waitFor(() => expect(result.current.isSuccess).toBe(true)); }); it('fails when server returns 400 (last owner)', async () => { server.use( http.delete(`${USERS_URL}/:userId`, () => HttpResponse.json( { title: 'Bad Request', detail: 'At least one Owner must remain.', status: 400 }, { status: 400 }, ), ), ); const { result } = renderHook(() => useDeleteUser(), { wrapper: createWrapper() }); await act(async () => { await result.current .mutateAsync('11111111-1111-1111-1111-111111111111') .catch(() => {}); }); await waitFor(() => expect(result.current.isError).toBe(true)); }); });