From e67fe73da7796bdfc50e506b7a9e7b2b94604b13 Mon Sep 17 00:00:00 2001 From: CCS Admin Date: Thu, 26 Feb 2026 21:21:27 +0000 Subject: [PATCH] test: add hook tests for useDisclosures, useAuditLog, useUsers Co-Authored-By: Claude Opus 4.6 --- .../src/hooks/__tests__/useAuditLog.test.ts | 116 ++++++++++++ .../hooks/__tests__/useDisclosures.test.ts | 146 +++++++++++++++ frontend/src/hooks/__tests__/useUsers.test.ts | 168 ++++++++++++++++++ 3 files changed, 430 insertions(+) create mode 100644 frontend/src/hooks/__tests__/useAuditLog.test.ts create mode 100644 frontend/src/hooks/__tests__/useDisclosures.test.ts create mode 100644 frontend/src/hooks/__tests__/useUsers.test.ts diff --git a/frontend/src/hooks/__tests__/useAuditLog.test.ts b/frontend/src/hooks/__tests__/useAuditLog.test.ts new file mode 100644 index 0000000..8ce9804 --- /dev/null +++ b/frontend/src/hooks/__tests__/useAuditLog.test.ts @@ -0,0 +1,116 @@ +import { describe, it, expect } from 'vitest' +import { waitFor } from '@testing-library/react' +import { http, HttpResponse } from 'msw' +import { server } from '@/test/mocks/server' +import { renderHookWithProviders } from '@/test/utils' +import { mockAuditLogEntry } from '@/test/mocks/data' +import { useAuditLog } from '@/hooks/useAuditLog' +import type { AuditLogFilters } from '@/hooks/useAuditLog' + +describe('useAuditLog', () => { + const defaultFilters: AuditLogFilters = { skip: 0, limit: 50 } + + it('fetches audit log entries (success)', async () => { + const { result } = renderHookWithProviders(() => useAuditLog(defaultFilters)) + + await waitFor(() => expect(result.current.isSuccess).toBe(true)) + + expect(result.current.data).toEqual([mockAuditLogEntry]) + expect(result.current.data).toHaveLength(1) + expect(result.current.data![0].action).toBe('case.update') + }) + + it('passes filter parameters (user_id, action, date_from, date_to, skip, limit)', async () => { + const capturedParams: Record = {} + + server.use( + http.get('/api/admin/audit-log', ({ request }) => { + const url = new URL(request.url) + capturedParams.skip = url.searchParams.get('skip') + capturedParams.limit = url.searchParams.get('limit') + capturedParams.user_id = url.searchParams.get('user_id') + capturedParams.action = url.searchParams.get('action') + capturedParams.date_from = url.searchParams.get('date_from') + capturedParams.date_to = url.searchParams.get('date_to') + return HttpResponse.json([mockAuditLogEntry]) + }), + ) + + const filters: AuditLogFilters = { + skip: 10, + limit: 25, + user_id: '1', + action: 'case.update', + date_from: '2026-02-01', + date_to: '2026-02-28', + } + + const { result } = renderHookWithProviders(() => useAuditLog(filters)) + + await waitFor(() => expect(result.current.isSuccess).toBe(true)) + + expect(capturedParams.skip).toBe('10') + expect(capturedParams.limit).toBe('25') + expect(capturedParams.user_id).toBe('1') + expect(capturedParams.action).toBe('case.update') + expect(capturedParams.date_from).toBe('2026-02-01') + expect(capturedParams.date_to).toBe('2026-02-28') + }) + + it('does not send optional params when they are undefined', async () => { + const capturedParams: Record = {} + + server.use( + http.get('/api/admin/audit-log', ({ request }) => { + const url = new URL(request.url) + capturedParams.skip = url.searchParams.get('skip') + capturedParams.limit = url.searchParams.get('limit') + capturedParams.user_id = url.searchParams.get('user_id') + capturedParams.action = url.searchParams.get('action') + capturedParams.date_from = url.searchParams.get('date_from') + capturedParams.date_to = url.searchParams.get('date_to') + return HttpResponse.json([]) + }), + ) + + const { result } = renderHookWithProviders(() => useAuditLog(defaultFilters)) + + await waitFor(() => expect(result.current.isSuccess).toBe(true)) + + expect(capturedParams.skip).toBe('0') + expect(capturedParams.limit).toBe('50') + expect(capturedParams.user_id).toBeNull() + expect(capturedParams.action).toBeNull() + expect(capturedParams.date_from).toBeNull() + expect(capturedParams.date_to).toBeNull() + }) + + it('handles empty response', async () => { + server.use( + http.get('/api/admin/audit-log', () => { + return HttpResponse.json([]) + }), + ) + + const { result } = renderHookWithProviders(() => useAuditLog(defaultFilters)) + + await waitFor(() => expect(result.current.isSuccess).toBe(true)) + + expect(result.current.data).toEqual([]) + expect(result.current.data).toHaveLength(0) + }) + + it('handles server error (500)', async () => { + server.use( + http.get('/api/admin/audit-log', () => { + return new HttpResponse(null, { status: 500 }) + }), + ) + + const { result } = renderHookWithProviders(() => useAuditLog(defaultFilters)) + + await waitFor(() => expect(result.current.isError).toBe(true)) + + expect(result.current.data).toBeUndefined() + }) +}) diff --git a/frontend/src/hooks/__tests__/useDisclosures.test.ts b/frontend/src/hooks/__tests__/useDisclosures.test.ts new file mode 100644 index 0000000..eb04172 --- /dev/null +++ b/frontend/src/hooks/__tests__/useDisclosures.test.ts @@ -0,0 +1,146 @@ +import { describe, it, expect } from 'vitest' +import { waitFor, act } from '@testing-library/react' +import { http, HttpResponse } from 'msw' +import { server } from '@/test/mocks/server' +import { renderHookWithProviders } from '@/test/utils' +import { + mockDisclosureRequest, + mockDisclosureApproved, +} from '@/test/mocks/data' +import { useDisclosures, useReviewDisclosure } from '@/hooks/useDisclosures' + +// --------------------------------------------------------------------------- +// useDisclosures +// --------------------------------------------------------------------------- + +describe('useDisclosures', () => { + it('fetches disclosures with status filter (success)', async () => { + let capturedStatus: string | null = null + + server.use( + http.get('/api/admin/disclosure-requests', ({ request }) => { + const url = new URL(request.url) + capturedStatus = url.searchParams.get('status') + return HttpResponse.json([mockDisclosureRequest]) + }), + ) + + const { result } = renderHookWithProviders(() => useDisclosures('pending')) + + await waitFor(() => expect(result.current.isSuccess).toBe(true)) + + expect(capturedStatus).toBe('pending') + expect(result.current.data).toEqual([mockDisclosureRequest]) + expect(result.current.data).toHaveLength(1) + expect(result.current.data![0].status).toBe('pending') + }) + + it('handles empty list', async () => { + server.use( + http.get('/api/admin/disclosure-requests', () => { + return HttpResponse.json([]) + }), + ) + + const { result } = renderHookWithProviders(() => useDisclosures('pending')) + + await waitFor(() => expect(result.current.isSuccess).toBe(true)) + + expect(result.current.data).toEqual([]) + expect(result.current.data).toHaveLength(0) + }) + + it('works without status parameter (uses default "pending")', async () => { + let capturedStatus: string | null = null + + server.use( + http.get('/api/admin/disclosure-requests', ({ request }) => { + const url = new URL(request.url) + capturedStatus = url.searchParams.get('status') + return HttpResponse.json([mockDisclosureRequest]) + }), + ) + + const { result } = renderHookWithProviders(() => useDisclosures()) + + await waitFor(() => expect(result.current.isSuccess).toBe(true)) + + expect(capturedStatus).toBe('pending') + expect(result.current.data).toHaveLength(1) + }) +}) + +// --------------------------------------------------------------------------- +// useReviewDisclosure +// --------------------------------------------------------------------------- + +describe('useReviewDisclosure', () => { + it('approves a disclosure (mutation success)', async () => { + server.use( + http.put('/api/admin/disclosure-requests/:id', () => { + return HttpResponse.json(mockDisclosureApproved) + }), + ) + + const { result } = renderHookWithProviders(() => useReviewDisclosure()) + + let response: unknown + await act(async () => { + response = await result.current.mutateAsync({ id: 1, status: 'approved' }) + }) + + await waitFor(() => expect(result.current.isSuccess).toBe(true)) + + expect((response as { status: string }).status).toBe('approved') + expect((response as { reviewed_by: number }).reviewed_by).toBe(1) + }) + + it('rejects a disclosure', async () => { + const rejectedDisclosure = { + ...mockDisclosureRequest, + status: 'rejected' as const, + reviewed_by: 1, + reviewed_at: '2026-02-26T09:30:00Z', + } + + server.use( + http.put('/api/admin/disclosure-requests/:id', () => { + return HttpResponse.json(rejectedDisclosure) + }), + ) + + const { result } = renderHookWithProviders(() => useReviewDisclosure()) + + let response: unknown + await act(async () => { + response = await result.current.mutateAsync({ id: 1, status: 'rejected' }) + }) + + await waitFor(() => expect(result.current.isSuccess).toBe(true)) + + expect((response as { status: string }).status).toBe('rejected') + }) + + it('handles error', async () => { + server.use( + http.put('/api/admin/disclosure-requests/:id', () => { + return HttpResponse.json( + { detail: 'Not found' }, + { status: 404 }, + ) + }), + ) + + const { result } = renderHookWithProviders(() => useReviewDisclosure()) + + await act(async () => { + try { + await result.current.mutateAsync({ id: 999, status: 'approved' }) + } catch { + // expected + } + }) + + await waitFor(() => expect(result.current.isError).toBe(true)) + }) +}) diff --git a/frontend/src/hooks/__tests__/useUsers.test.ts b/frontend/src/hooks/__tests__/useUsers.test.ts new file mode 100644 index 0000000..db5cc3f --- /dev/null +++ b/frontend/src/hooks/__tests__/useUsers.test.ts @@ -0,0 +1,168 @@ +import { describe, it, expect } from 'vitest' +import { waitFor, act } from '@testing-library/react' +import { http, HttpResponse } from 'msw' +import { server } from '@/test/mocks/server' +import { renderHookWithProviders } from '@/test/utils' +import { mockUserResponse } from '@/test/mocks/data' +import { useUsers, useCreateUser, useUpdateUser } from '@/hooks/useUsers' + +// --------------------------------------------------------------------------- +// useUsers +// --------------------------------------------------------------------------- + +describe('useUsers', () => { + it('fetches user list (success)', async () => { + const { result } = renderHookWithProviders(() => useUsers()) + + await waitFor(() => expect(result.current.isSuccess).toBe(true)) + + expect(result.current.data).toEqual([mockUserResponse]) + expect(result.current.data).toHaveLength(1) + expect(result.current.data![0].username).toBe('admin_user') + expect(result.current.data![0].role).toBe('admin') + }) + + it('handles empty list', async () => { + server.use( + http.get('/api/admin/users', () => { + return HttpResponse.json([]) + }), + ) + + const { result } = renderHookWithProviders(() => useUsers()) + + await waitFor(() => expect(result.current.isSuccess).toBe(true)) + + expect(result.current.data).toEqual([]) + expect(result.current.data).toHaveLength(0) + }) +}) + +// --------------------------------------------------------------------------- +// useCreateUser +// --------------------------------------------------------------------------- + +describe('useCreateUser', () => { + it('creates user (success)', async () => { + const newUser = { + ...mockUserResponse, + id: 3, + username: 'new_user', + email: 'new@dak.de', + role: 'dak_mitarbeiter' as const, + } + + server.use( + http.post('/api/admin/users', () => { + return HttpResponse.json(newUser) + }), + ) + + const { result } = renderHookWithProviders(() => useCreateUser()) + + let response: unknown + await act(async () => { + response = await result.current.mutateAsync({ + username: 'new_user', + email: 'new@dak.de', + password: 'SecurePass123!', + role: 'dak_mitarbeiter', + }) + }) + + await waitFor(() => expect(result.current.isSuccess).toBe(true)) + + expect((response as { id: number }).id).toBe(3) + expect((response as { username: string }).username).toBe('new_user') + expect((response as { role: string }).role).toBe('dak_mitarbeiter') + }) + + it('handles validation error (422)', async () => { + server.use( + http.post('/api/admin/users', () => { + return HttpResponse.json( + { detail: 'Username already exists' }, + { status: 422 }, + ) + }), + ) + + const { result } = renderHookWithProviders(() => useCreateUser()) + + await act(async () => { + try { + await result.current.mutateAsync({ + username: 'admin_user', + email: 'duplicate@dak.de', + password: 'SecurePass123!', + role: 'admin', + }) + } catch { + // expected + } + }) + + await waitFor(() => expect(result.current.isError).toBe(true)) + }) +}) + +// --------------------------------------------------------------------------- +// useUpdateUser +// --------------------------------------------------------------------------- + +describe('useUpdateUser', () => { + it('updates user (success)', async () => { + const updatedUser = { + ...mockUserResponse, + role: 'dak_mitarbeiter' as const, + is_active: false, + } + + server.use( + http.put('/api/admin/users/:id', () => { + return HttpResponse.json(updatedUser) + }), + ) + + const { result } = renderHookWithProviders(() => useUpdateUser()) + + let response: unknown + await act(async () => { + response = await result.current.mutateAsync({ + id: 1, + payload: { role: 'dak_mitarbeiter', is_active: false }, + }) + }) + + await waitFor(() => expect(result.current.isSuccess).toBe(true)) + + expect((response as { role: string }).role).toBe('dak_mitarbeiter') + expect((response as { is_active: boolean }).is_active).toBe(false) + }) + + it('handles error (404 Not Found)', async () => { + server.use( + http.put('/api/admin/users/:id', () => { + return HttpResponse.json( + { detail: 'User not found' }, + { status: 404 }, + ) + }), + ) + + const { result } = renderHookWithProviders(() => useUpdateUser()) + + await act(async () => { + try { + await result.current.mutateAsync({ + id: 999, + payload: { is_active: false }, + }) + } catch { + // expected + } + }) + + await waitFor(() => expect(result.current.isError).toBe(true)) + }) +})