adds unit tests to cover 80%
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
import { act, renderHook, waitFor } from '@testing-library/react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { createElement } from 'react';
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { server } from '@/mocks/server';
|
||||
import { API_BASE } from '@/mocks/auth/fixtures';
|
||||
import { useValidateInvitation, useCompleteInvitation } from '@/api/useInvitation';
|
||||
|
||||
function createWrapper() {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false }, mutations: { retry: false } },
|
||||
});
|
||||
return ({ children }: { children: React.ReactNode }) =>
|
||||
createElement(QueryClientProvider, { client: queryClient }, children);
|
||||
}
|
||||
|
||||
describe('useValidateInvitation', () => {
|
||||
it('returns isValid=true for a valid token', async () => {
|
||||
const { result } = renderHook(() => useValidateInvitation('valid-token'), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await waitFor(() => expect(result.current.isPending).toBe(false));
|
||||
|
||||
expect(result.current.data?.isValid).toBe(true);
|
||||
expect(result.current.data?.email).toBe('invited@example.com');
|
||||
});
|
||||
|
||||
it('returns isValid=false for an expired or unknown token', async () => {
|
||||
const { result } = renderHook(() => useValidateInvitation('expired-token'), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await waitFor(() => expect(result.current.isPending).toBe(false));
|
||||
|
||||
expect(result.current.data?.isValid).toBe(false);
|
||||
});
|
||||
|
||||
it('does not fetch when token is null', () => {
|
||||
const { result } = renderHook(() => useValidateInvitation(null), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
expect(result.current.isPending).toBe(true);
|
||||
expect(result.current.fetchStatus).toBe('idle');
|
||||
});
|
||||
|
||||
it('enters error state on server error', async () => {
|
||||
server.use(
|
||||
http.get(`${API_BASE}/api/v1/Invitation/validate`, () =>
|
||||
HttpResponse.json({}, { status: 500 }),
|
||||
),
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useValidateInvitation('any-token'), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await waitFor(() => expect(result.current.isPending).toBe(false));
|
||||
|
||||
expect(result.current.isError).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useCompleteInvitation', () => {
|
||||
it('succeeds with valid token, name, and password', async () => {
|
||||
const { result } = renderHook(() => useCompleteInvitation(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.mutateAsync({
|
||||
token: 'valid-token',
|
||||
name: 'New User',
|
||||
password: 'Password1!',
|
||||
});
|
||||
});
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
});
|
||||
|
||||
it('enters error state when token is invalid', async () => {
|
||||
const { result } = renderHook(() => useCompleteInvitation(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current
|
||||
.mutateAsync({ token: 'bad-token', name: 'User', password: 'Password1!' })
|
||||
.catch(() => {});
|
||||
});
|
||||
|
||||
await waitFor(() => expect(result.current.isError).toBe(true));
|
||||
});
|
||||
|
||||
it('enters error state on server error', async () => {
|
||||
server.use(
|
||||
http.post(`${API_BASE}/api/v1/Invitation/complete`, () =>
|
||||
HttpResponse.json({}, { status: 500 }),
|
||||
),
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useCompleteInvitation(), {
|
||||
wrapper: createWrapper(),
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current
|
||||
.mutateAsync({ token: 'valid-token', name: 'User', password: 'Password1!' })
|
||||
.catch(() => {});
|
||||
});
|
||||
|
||||
await waitFor(() => expect(result.current.isError).toBe(true));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
import { act, renderHook, waitFor } from '@testing-library/react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { createElement } from 'react';
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { server } from '@/mocks/server';
|
||||
import { API_BASE } from '@/mocks/auth/fixtures';
|
||||
import { useUpdateProfile, useChangePassword } from '@/api/useProfile';
|
||||
import { AuthProvider } from '@/contexts/AuthProvider';
|
||||
import i18n from '@/i18n/config';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
function createWrapper() {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false }, mutations: { retry: false } },
|
||||
});
|
||||
return ({ children }: { children: ReactNode }) =>
|
||||
createElement(
|
||||
QueryClientProvider,
|
||||
{ client: queryClient },
|
||||
createElement(
|
||||
I18nextProvider,
|
||||
{ i18n },
|
||||
createElement(AuthProvider, null, children),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
describe('useUpdateProfile', () => {
|
||||
it('succeeds and returns updated user', async () => {
|
||||
const { result } = renderHook(() => useUpdateProfile(), { wrapper: createWrapper() });
|
||||
|
||||
await act(async () => {
|
||||
await result.current.mutateAsync({ name: 'New Name', email: 'new@example.com' });
|
||||
});
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
expect(result.current.data?.name).toBe('New Name');
|
||||
});
|
||||
|
||||
it('enters error state on server failure', async () => {
|
||||
server.use(
|
||||
http.put(`${API_BASE}/api/v1/Users/me`, () =>
|
||||
HttpResponse.json({ title: 'Bad Request', status: 400 }, { status: 400 }),
|
||||
),
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useUpdateProfile(), { wrapper: createWrapper() });
|
||||
|
||||
await act(async () => {
|
||||
await result.current
|
||||
.mutateAsync({ name: 'Bad', email: 'bad@example.com' })
|
||||
.catch(() => {});
|
||||
});
|
||||
|
||||
await waitFor(() => expect(result.current.isError).toBe(true));
|
||||
});
|
||||
});
|
||||
|
||||
describe('useChangePassword', () => {
|
||||
it('succeeds with correct current password', async () => {
|
||||
const { result } = renderHook(() => useChangePassword(), { wrapper: createWrapper() });
|
||||
|
||||
await act(async () => {
|
||||
await result.current.mutateAsync({
|
||||
currentPassword: 'Password123!',
|
||||
newPassword: 'NewPassword1!',
|
||||
});
|
||||
});
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
});
|
||||
|
||||
it('enters error state when current password is wrong', async () => {
|
||||
const { result } = renderHook(() => useChangePassword(), { wrapper: createWrapper() });
|
||||
|
||||
await act(async () => {
|
||||
await result.current
|
||||
.mutateAsync({ currentPassword: 'WrongPassword!', newPassword: 'NewPassword1!' })
|
||||
.catch(() => {});
|
||||
});
|
||||
|
||||
await waitFor(() => expect(result.current.isError).toBe(true));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,91 @@
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { server } from '@/mocks/server';
|
||||
import { API_BASE } from '@/mocks/auth/fixtures';
|
||||
import { useSetup } from '@/api/useSetup';
|
||||
|
||||
const validSetupData = {
|
||||
name: 'Test Owner',
|
||||
email: 'owner@example.com',
|
||||
password: 'Password1!',
|
||||
language: 'nl' as const,
|
||||
};
|
||||
|
||||
describe('useSetup', () => {
|
||||
it('starts with isPending=false and no error', () => {
|
||||
const { result } = renderHook(() => useSetup());
|
||||
|
||||
expect(result.current.isPending).toBe(false);
|
||||
expect(result.current.error).toBeNull();
|
||||
});
|
||||
|
||||
it('returns setup response on success', async () => {
|
||||
const { result } = renderHook(() => useSetup());
|
||||
|
||||
let response: Awaited<ReturnType<typeof result.current.mutateAsync>> | undefined;
|
||||
await act(async () => {
|
||||
response = await result.current.mutateAsync(validSetupData);
|
||||
});
|
||||
|
||||
expect(response?.message).toBe('System initialized');
|
||||
expect(response?.user.role).toBe('Owner');
|
||||
expect(result.current.isPending).toBe(false);
|
||||
expect(result.current.error).toBeNull();
|
||||
});
|
||||
|
||||
it('sets error and rethrows when API fails', async () => {
|
||||
server.use(
|
||||
http.post(`${API_BASE}/api/v1/Setup/owner`, () =>
|
||||
HttpResponse.json(
|
||||
{ title: 'Conflict', detail: 'Already initialized', status: 409 },
|
||||
{ status: 409 },
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useSetup());
|
||||
|
||||
let thrown: Error | undefined;
|
||||
await act(async () => {
|
||||
try {
|
||||
await result.current.mutateAsync(validSetupData);
|
||||
} catch (err) {
|
||||
thrown = err as Error;
|
||||
}
|
||||
});
|
||||
|
||||
expect(thrown).toBeDefined();
|
||||
expect(result.current.error).not.toBeNull();
|
||||
expect(result.current.isPending).toBe(false);
|
||||
});
|
||||
|
||||
it('resets error state on subsequent successful call', async () => {
|
||||
server.use(
|
||||
http.post(`${API_BASE}/api/v1/Setup/owner`, () =>
|
||||
HttpResponse.json({ title: 'Error', status: 500 }, { status: 500 }),
|
||||
),
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useSetup());
|
||||
|
||||
await act(async () => {
|
||||
try {
|
||||
await result.current.mutateAsync(validSetupData);
|
||||
} catch {
|
||||
// expected
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.current.error).not.toBeNull();
|
||||
|
||||
// Restore success handler and retry
|
||||
server.resetHandlers();
|
||||
|
||||
await act(async () => {
|
||||
await result.current.mutateAsync(validSetupData);
|
||||
});
|
||||
|
||||
expect(result.current.error).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -6,7 +6,7 @@ import { describe, expect, it } from 'vitest';
|
||||
import { server } from '@/mocks/server';
|
||||
import { API_BASE } from '@/mocks/auth/fixtures';
|
||||
import { getMockUsers } from '@/mocks/users/handlers';
|
||||
import { useUsers, useInviteUser, useChangeRole } from './useUsers';
|
||||
import { useUsers, useInviteUser, useChangeRole, useSetUserActive, useDeleteUser } from './useUsers';
|
||||
|
||||
function createWrapper() {
|
||||
const queryClient = new QueryClient({
|
||||
@@ -102,3 +102,67 @@ describe('useChangeRole', () => {
|
||||
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));
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user