adds unit tests to cover 80%

This commit is contained in:
2026-06-26 00:27:27 +02:00
parent b84e2f8e3e
commit 0e01ca1e1c
69 changed files with 90596 additions and 46 deletions
+116
View File
@@ -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));
});
});
+86
View File
@@ -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));
});
});
+91
View File
@@ -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();
});
});
+65 -1
View File
@@ -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));
});
});
+78
View File
@@ -0,0 +1,78 @@
import { renderHook, waitFor } from '@testing-library/react';
import { http, HttpResponse } from 'msw';
import { beforeEach, describe, expect, it } from 'vitest';
import { server } from '@/mocks/server';
import { API_BASE } from '@/mocks/auth/fixtures';
import { useInitGuard, _resetSetupStatusCache } from '@/auth/useInitGuard';
const SETUP_STATUS_URL = `${API_BASE}/Setup/status`;
beforeEach(() => {
_resetSetupStatusCache();
});
describe('useInitGuard', () => {
it('starts in loading state', () => {
const { result } = renderHook(() => useInitGuard());
expect(result.current.isLoading).toBe(true);
expect(result.current.initialized).toBeNull();
expect(result.current.error).toBeNull();
});
it('returns initialized=true when system is set up', async () => {
server.use(
http.get(SETUP_STATUS_URL, () => HttpResponse.json({ initialized: true })),
);
const { result } = renderHook(() => useInitGuard());
await waitFor(() => expect(result.current.isLoading).toBe(false));
expect(result.current.initialized).toBe(true);
expect(result.current.error).toBeNull();
});
it('returns initialized=false when system is not yet set up', async () => {
server.use(
http.get(SETUP_STATUS_URL, () => HttpResponse.json({ initialized: false })),
);
const { result } = renderHook(() => useInitGuard());
await waitFor(() => expect(result.current.isLoading).toBe(false));
expect(result.current.initialized).toBe(false);
expect(result.current.error).toBeNull();
});
it('sets error when the API request fails', async () => {
server.use(http.get(SETUP_STATUS_URL, () => HttpResponse.error()));
const { result } = renderHook(() => useInitGuard());
await waitFor(() => expect(result.current.isLoading).toBe(false));
expect(result.current.error).not.toBeNull();
expect(result.current.initialized).toBeNull();
});
it('uses cached result on second call without re-fetching', async () => {
let callCount = 0;
server.use(
http.get(SETUP_STATUS_URL, () => {
callCount += 1;
return HttpResponse.json({ initialized: true });
}),
);
const { result: first } = renderHook(() => useInitGuard());
await waitFor(() => expect(first.current.isLoading).toBe(false));
const { result: second } = renderHook(() => useInitGuard());
await waitFor(() => expect(second.current.isLoading).toBe(false));
expect(callCount).toBe(1); // Only one fetch due to cache
expect(second.current.initialized).toBe(true);
});
});
@@ -0,0 +1,38 @@
import { beforeEach, describe, expect, it } from 'vitest';
import { screen } from '@testing-library/react';
import { http, HttpResponse } from 'msw';
import { renderApp } from '@/test/utils';
import { server } from '@/mocks/server';
import { API_BASE, makeAuthResponse, mockUser } from '@/mocks/auth/fixtures';
import { _resetSetupStatusCache } from '@/router';
beforeEach(() => {
_resetSetupStatusCache();
});
describe('RoleGuard', () => {
it('renders protected content when user has an allowed role (Owner on /users)', async () => {
// Default auth mock returns Owner role, /users requires Owner or Administrator
server.use(
http.post(`${API_BASE}/api/v1/auth/refresh`, () =>
HttpResponse.json(makeAuthResponse()),
),
);
renderApp('/users');
expect(await screen.findByTestId('users-title')).toBeInTheDocument();
});
it('shows access denied message when user lacks the required role', async () => {
// Authenticate with 'User' role — /users requires Owner or Administrator
server.use(
http.post(`${API_BASE}/api/v1/auth/refresh`, () =>
HttpResponse.json(makeAuthResponse({ user: { ...mockUser, role: 'User' } })),
),
);
renderApp('/users');
expect(await screen.findByTestId('access-denied-message')).toBeInTheDocument();
expect(screen.queryByTestId('users-title')).not.toBeInTheDocument();
});
});
@@ -56,3 +56,29 @@ describe('AppLayout auth guard', () => {
expect(await screen.findByTestId('login-form-submit-button')).toBeInTheDocument();
});
});
describe('UserMenu', () => {
it('shows user name in menu after opening', async () => {
mockAuthenticated();
const user = userEvent.setup();
renderApp('/dashboard');
await screen.findByTestId('user-menu-button');
await user.click(screen.getByTestId('user-menu-button'));
expect(await screen.findByTestId('user-menu-name')).toBeInTheDocument();
});
it('logs out and redirects to login when logout is clicked', async () => {
mockAuthenticated();
const user = userEvent.setup();
renderApp('/dashboard');
await screen.findByTestId('user-menu-button');
await user.click(screen.getByTestId('user-menu-button'));
await screen.findByTestId('user-menu-logout');
await user.click(screen.getByTestId('user-menu-logout'));
expect(await screen.findByTestId('login-form-submit-button')).toBeInTheDocument();
});
});
@@ -0,0 +1,36 @@
import { describe, expect, it, vi } from 'vitest';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { FormBannerError } from '@/components/ui/FormBannerError';
describe('FormBannerError', () => {
it('renders nothing when error is null', () => {
const { container } = render(
<FormBannerError error={null} onDismiss={vi.fn()} />,
);
expect(container.firstChild).toBeNull();
});
it('renders the error message when error is provided', () => {
render(
<FormBannerError
error={{ message: 'Something went wrong' }}
onDismiss={vi.fn()}
/>,
);
expect(screen.getByTestId('form-error-banner')).toBeInTheDocument();
expect(screen.getByText('Something went wrong')).toBeInTheDocument();
});
it('calls onDismiss when the dismiss button is clicked', async () => {
const onDismiss = vi.fn();
render(
<FormBannerError
error={{ message: 'Error to dismiss' }}
onDismiss={onDismiss}
/>,
);
await userEvent.click(screen.getByRole('button', { name: /dismiss/i }));
expect(onDismiss).toHaveBeenCalledOnce();
});
});
+186
View File
@@ -0,0 +1,186 @@
import { describe, expect, it, vi } from 'vitest';
import { http, HttpResponse } from 'msw';
import { server } from '@/mocks/server';
import { ApiClient, ProblemDetailsError, NetworkError } from '@/lib/api-client';
const BASE = 'http://localhost:5000';
function makeClient() {
return new ApiClient(BASE);
}
describe('ApiClient', () => {
it('makes a GET request and parses JSON response', async () => {
server.use(http.get(`${BASE}/test`, () => HttpResponse.json({ ok: true })));
const client = makeClient();
const result = await client.get<{ ok: boolean }>('/test');
expect(result).toEqual({ ok: true });
});
it('makes a POST request with JSON body', async () => {
server.use(
http.post(`${BASE}/test`, async ({ request }) => {
const body = (await request.json()) as { name: string };
return HttpResponse.json({ received: body.name });
}),
);
const client = makeClient();
const result = await client.post<{ received: string }>('/test', { name: 'hello' });
expect(result.received).toBe('hello');
});
it('makes a PUT request', async () => {
server.use(http.put(`${BASE}/item/1`, () => HttpResponse.json({ updated: true })));
const client = makeClient();
const result = await client.put<{ updated: boolean }>('/item/1', { value: 'x' });
expect(result.updated).toBe(true);
});
it('returns undefined for 204 No Content responses', async () => {
server.use(http.delete(`${BASE}/item/1`, () => new HttpResponse(null, { status: 204 })));
const client = makeClient();
const result = await client.delete<undefined>('/item/1');
expect(result).toBeUndefined();
});
it('throws ProblemDetailsError on non-2xx response', async () => {
server.use(
http.get(`${BASE}/notfound`, () =>
HttpResponse.json({ title: 'Not Found', status: 404 }, { status: 404 }),
),
);
const client = makeClient();
await expect(client.get('/notfound')).rejects.toThrow(ProblemDetailsError);
});
it('ProblemDetailsError carries status and problem details', async () => {
server.use(
http.get(`${BASE}/bad`, () =>
HttpResponse.json({ title: 'Bad Request', status: 400, detail: 'Invalid input' }, { status: 400 }),
),
);
const client = makeClient();
try {
await client.get('/bad');
expect.fail('should have thrown');
} catch (err) {
expect(err).toBeInstanceOf(ProblemDetailsError);
const pde = err as ProblemDetailsError;
expect(pde.status).toBe(400);
expect(pde.problem.title).toBe('Bad Request');
}
});
it('injects Bearer token when access token is set', async () => {
let capturedAuth: string | null = null;
server.use(
http.get(`${BASE}/secured`, ({ request }) => {
capturedAuth = request.headers.get('Authorization');
return HttpResponse.json({});
}),
);
const client = makeClient();
client.setAccessToken('my-token');
await client.get('/secured');
expect(capturedAuth).toBe('Bearer my-token');
});
it('does not inject auth header when no access token is set', async () => {
let capturedAuth: string | null = null;
server.use(
http.get(`${BASE}/public`, ({ request }) => {
capturedAuth = request.headers.get('Authorization');
return HttpResponse.json({});
}),
);
const client = makeClient();
await client.get('/public');
expect(capturedAuth).toBeNull();
});
it('clears token when setAccessToken(null) is called', async () => {
let capturedAuth: string | null = null;
server.use(
http.get(`${BASE}/cleared`, ({ request }) => {
capturedAuth = request.headers.get('Authorization');
return HttpResponse.json({});
}),
);
const client = makeClient();
client.setAccessToken('my-token');
client.setAccessToken(null);
await client.get('/cleared');
expect(capturedAuth).toBeNull();
});
it('retries once with new token after 401', async () => {
let callCount = 0;
server.use(
http.get(`${BASE}/protected`, () => {
callCount += 1;
if (callCount === 1) return new HttpResponse(null, { status: 401 });
return HttpResponse.json({ secret: 'data' });
}),
);
const client = makeClient();
client.setRefreshHandler(async () => 'new-token');
const result = await client.get<{ secret: string }>('/protected');
expect(result.secret).toBe('data');
expect(callCount).toBe(2);
});
it('calls authFailureHandler and throws when refresh returns null', async () => {
server.use(http.get(`${BASE}/protected`, () => new HttpResponse(null, { status: 401 })));
const client = makeClient();
const failureFn = vi.fn();
client.setRefreshHandler(async () => null);
client.setAuthFailureHandler(failureFn);
await expect(client.get('/protected')).rejects.toThrow(ProblemDetailsError);
expect(failureFn).toHaveBeenCalledOnce();
});
it('does not retry 401 when skipAuthRefresh is set', async () => {
let callCount = 0;
server.use(
http.get(`${BASE}/skip`, () => {
callCount += 1;
return new HttpResponse(null, { status: 401 });
}),
);
const client = makeClient();
client.setRefreshHandler(async () => 'new-token');
await expect(
client.get('/skip', { skipAuthRefresh: true }),
).rejects.toThrow(ProblemDetailsError);
expect(callCount).toBe(1);
});
it('throws NetworkError when fetch fails', async () => {
server.use(http.get(`${BASE}/network-fail`, () => HttpResponse.error()));
const client = makeClient();
await expect(client.get('/network-fail')).rejects.toThrow(NetworkError);
});
});
+122
View File
@@ -0,0 +1,122 @@
import { describe, expect, it } from 'vitest';
import {
passwordSchema,
setupFormSchema,
loginFormSchema,
invitationCompleteFormSchema,
} from '@/lib/schemas/auth';
describe('passwordSchema', () => {
it('accepts a valid password', () => {
expect(passwordSchema.safeParse('Password1!').success).toBe(true);
});
it('rejects a password that is too short', () => {
expect(passwordSchema.safeParse('Pass1!').success).toBe(false);
});
it('rejects a password without an uppercase letter', () => {
expect(passwordSchema.safeParse('password1!').success).toBe(false);
});
it('rejects a password without a digit', () => {
expect(passwordSchema.safeParse('Password!').success).toBe(false);
});
it('rejects a password without a special character', () => {
expect(passwordSchema.safeParse('Password1').success).toBe(false);
});
});
describe('setupFormSchema', () => {
const valid = {
name: 'Admin',
email: 'admin@example.com',
password: 'Password1!',
confirmPassword: 'Password1!',
language: 'nl' as const,
};
it('accepts valid setup data', () => {
expect(setupFormSchema.safeParse(valid).success).toBe(true);
});
it('rejects when passwords do not match', () => {
const result = setupFormSchema.safeParse({ ...valid, confirmPassword: 'Different1!' });
expect(result.success).toBe(false);
});
it('rejects empty name', () => {
expect(setupFormSchema.safeParse({ ...valid, name: '' }).success).toBe(false);
});
it('rejects name exceeding 255 characters', () => {
expect(setupFormSchema.safeParse({ ...valid, name: 'a'.repeat(256) }).success).toBe(false);
});
it('rejects invalid email format', () => {
expect(setupFormSchema.safeParse({ ...valid, email: 'not-an-email' }).success).toBe(false);
});
it('rejects empty email', () => {
expect(setupFormSchema.safeParse({ ...valid, email: '' }).success).toBe(false);
});
it('rejects invalid language', () => {
expect(setupFormSchema.safeParse({ ...valid, language: 'fr' }).success).toBe(false);
});
it('accepts "en" as language', () => {
expect(setupFormSchema.safeParse({ ...valid, language: 'en' }).success).toBe(true);
});
});
describe('loginFormSchema', () => {
it('accepts valid login credentials', () => {
expect(loginFormSchema.safeParse({ email: 'user@example.com', password: 'anypass' }).success).toBe(true);
});
it('rejects empty email', () => {
expect(loginFormSchema.safeParse({ email: '', password: 'pass' }).success).toBe(false);
});
it('rejects invalid email format', () => {
expect(loginFormSchema.safeParse({ email: 'not-email', password: 'pass' }).success).toBe(false);
});
it('rejects empty password', () => {
expect(loginFormSchema.safeParse({ email: 'user@example.com', password: '' }).success).toBe(false);
});
});
describe('invitationCompleteFormSchema', () => {
const valid = {
name: 'New User',
password: 'Password1!',
confirmPassword: 'Password1!',
};
it('accepts valid invitation completion data', () => {
expect(invitationCompleteFormSchema.safeParse(valid).success).toBe(true);
});
it('rejects when passwords do not match', () => {
expect(
invitationCompleteFormSchema.safeParse({ ...valid, confirmPassword: 'Different1!' }).success,
).toBe(false);
});
it('rejects empty name', () => {
expect(invitationCompleteFormSchema.safeParse({ ...valid, name: '' }).success).toBe(false);
});
it('rejects weak password', () => {
expect(
invitationCompleteFormSchema.safeParse({
...valid,
password: 'weak',
confirmPassword: 'weak',
}).success,
).toBe(false);
});
});
+68
View File
@@ -0,0 +1,68 @@
import { beforeEach, describe, expect, it } from 'vitest';
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { http, HttpResponse } from 'msw';
import { renderApp, mockAuthenticated, mockGuest } from '@/test/utils';
import { server } from '@/mocks/server';
import { _resetSetupStatusCache } from '@/router';
beforeEach(() => {
_resetSetupStatusCache();
});
describe('DashboardPage', () => {
it('renders title and welcome message for authenticated user', async () => {
mockAuthenticated();
renderApp('/dashboard');
expect(await screen.findByTestId('dashboard-title')).toBeInTheDocument();
expect(screen.getByTestId('dashboard-welcome')).toBeInTheDocument();
});
it('displays the availability status badge when data loads', async () => {
mockAuthenticated();
renderApp('/dashboard');
await screen.findByTestId('dashboard-title');
// Availability badge should eventually appear after status loads
expect(await screen.findByTestId('availability-badge')).toBeInTheDocument();
});
it('shows retry button when availability fetch fails', async () => {
server.use(http.get('*/Availability/status', () => HttpResponse.error()));
mockAuthenticated();
renderApp('/dashboard');
await screen.findByTestId('dashboard-title');
expect(await screen.findByTestId('availability-retry')).toBeInTheDocument();
});
it('refetches availability when retry button is clicked', async () => {
let callCount = 0;
server.use(
http.get('*/Availability/status', () => {
callCount += 1;
if (callCount === 1) return HttpResponse.error();
return HttpResponse.json({ status: 'Available', checkedAt: new Date().toISOString(), message: '' });
}),
);
mockAuthenticated();
renderApp('/dashboard');
const retryButton = await screen.findByTestId('availability-retry');
await userEvent.click(retryButton);
expect(callCount).toBeGreaterThanOrEqual(2);
});
it('redirects unauthenticated users to login', async () => {
mockGuest();
renderApp('/dashboard');
expect(await screen.findByTestId('login-form-submit-button')).toBeInTheDocument();
expect(screen.queryByTestId('dashboard-title')).not.toBeInTheDocument();
});
});