Restructures frontend to a feature-based folder layout
Continuous Integration / config (pull_request) Successful in 11s
Continuous Integration / changes (pull_request) Successful in 21s
Continuous Integration / backend-build (pull_request) Skipped
Continuous Integration / backend-test (pull_request) Skipped
Continuous Integration / vulnerability-scan (pull_request) Skipped
Continuous Integration / frontend-prepare (pull_request) Successful in 1m37s
Continuous Integration / frontend-build (pull_request) Successful in 2m12s
Continuous Integration / frontend-test (pull_request) Successful in 4m42s
Continuous Integration / frontend-lint (pull_request) Successful in 2m2s
Continuous Integration / publish-production (pull_request) Skipped
Continuous Integration / deploy-production (pull_request) Skipped
Continuous Integration / publish-test (pull_request) Successful in 6m51s
Continuous Integration / deploy-test (pull_request) Skipped
Continuous Integration / config (pull_request) Successful in 11s
Continuous Integration / changes (pull_request) Successful in 21s
Continuous Integration / backend-build (pull_request) Skipped
Continuous Integration / backend-test (pull_request) Skipped
Continuous Integration / vulnerability-scan (pull_request) Skipped
Continuous Integration / frontend-prepare (pull_request) Successful in 1m37s
Continuous Integration / frontend-build (pull_request) Successful in 2m12s
Continuous Integration / frontend-test (pull_request) Successful in 4m42s
Continuous Integration / frontend-lint (pull_request) Successful in 2m2s
Continuous Integration / publish-production (pull_request) Skipped
Continuous Integration / deploy-production (pull_request) Skipped
Continuous Integration / publish-test (pull_request) Successful in 6m51s
Continuous Integration / deploy-test (pull_request) Skipped
Groups auth, setup, invitation, profile, users, cms, availability and system code (services/hooks, components, schemas, mocks, pages) under src/features/<name> instead of splitting by technical layer (api/, components/, lib/schemas/, mocks/, pages/). Renames the old connection-oriented `api` layer to `services` per feature, and splits the monolithic api/types.ts into per-feature types.ts files (with ProblemDetails/ApiResult merged into lib/api-client.ts as shared infra). Layout-agnostic code (ui primitives, app shell, i18n, test utils, lib) stays at the top level. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
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));
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user