diff --git a/frontend/src/test/mocks/data.ts b/frontend/src/test/mocks/data.ts new file mode 100644 index 0000000..320aa9a --- /dev/null +++ b/frontend/src/test/mocks/data.ts @@ -0,0 +1,272 @@ +import type { + User, + TokenResponse, + Case, + CaseListResponse, + DashboardKPIs, + WeeklyDataPoint, + DashboardResponse, + Notification, + NotificationList, + ReportMeta, + UserResponse, + AuditLogEntry, + DisclosureRequest, +} from '@/types' + +// ---------- Users ---------- + +export const mockUser: User = { + id: 1, + username: 'admin_user', + email: 'admin@dak.de', + first_name: 'Max', + last_name: 'Mustermann', + display_name: 'Max Mustermann', + avatar_url: null, + role: 'admin', + mfa_enabled: false, + is_active: true, + last_login: '2026-02-25T10:30:00Z', + created_at: '2026-01-01T08:00:00Z', +} + +export const mockDakUser: User = { + id: 2, + username: 'dak_user', + email: 'mitarbeiter@dak.de', + first_name: 'Anna', + last_name: 'Schmidt', + display_name: 'Anna Schmidt', + avatar_url: null, + role: 'dak_mitarbeiter', + mfa_enabled: false, + is_active: true, + last_login: '2026-02-24T14:00:00Z', + created_at: '2026-01-15T09:00:00Z', +} + +// ---------- Auth ---------- + +export const mockTokenResponse: TokenResponse = { + access_token: 'mock-access-token-abc123', + refresh_token: 'mock-refresh-token-xyz789', + token_type: 'bearer', + user: mockUser, +} + +// ---------- Cases ---------- + +export const mockCase: Case = { + id: 1, + fall_id: '2026-06-onko-A123456789', + crm_ticket_id: 'CRM-2026-001', + jahr: 2026, + kw: 6, + datum: '2026-02-05', + anrede: 'Herr', + vorname: 'Hans', + nachname: 'Meier', + geburtsdatum: '1965-03-15', + kvnr: 'A123456789', + versicherung: 'DAK-Gesundheit', + icd: 'C50.4', + fallgruppe: 'onko', + strasse: 'Musterstraße 12', + plz: '20095', + ort: 'Hamburg', + email: 'hans.meier@example.de', + ansprechpartner: 'Dr. Müller', + telefonnummer: '040-12345678', + mobiltelefon: '0151-12345678', + unterlagen: true, + unterlagen_verschickt: '2026-02-06', + erhalten: true, + unterlagen_erhalten: '2026-02-08', + unterlagen_an_gutachter: '2026-02-09', + gutachten: true, + gutachter: 'Prof. Dr. Weber', + gutachten_erstellt: '2026-02-15', + gutachten_versendet: '2026-02-16', + schweigepflicht: true, + ablehnung: false, + abbruch: false, + abbruch_datum: null, + gutachten_typ: 'Zweitmeinung', + therapieaenderung: 'Ja', + ta_diagnosekorrektur: false, + ta_unterversorgung: true, + ta_uebertherapie: false, + kurzbeschreibung: 'Mammakarzinom links, geplante Chemotherapie', + fragestellung: 'Ist die empfohlene Chemotherapie indiziert?', + kommentar: null, + sonstiges: null, + abgerechnet: false, + abrechnung_datum: null, + import_source: 'excel_import', + imported_at: '2026-02-05T09:00:00Z', + updated_at: '2026-02-16T14:30:00Z', + disclosure_granted: false, + disclosure_expires_at: null, +} + +export const mockCaseNoIcd: Case = { + ...mockCase, + id: 2, + fall_id: '2026-07-ortho-B987654321', + kvnr: 'B987654321', + icd: null, + fallgruppe: 'ortho', + kw: 7, + datum: '2026-02-12', + vorname: 'Maria', + nachname: 'Fischer', + anrede: 'Frau', + geburtsdatum: '1978-11-22', + gutachten: false, + gutachter: null, + gutachten_erstellt: null, + gutachten_versendet: null, + gutachten_typ: null, + therapieaenderung: null, + ta_diagnosekorrektur: false, + ta_unterversorgung: false, + ta_uebertherapie: false, + kurzbeschreibung: 'Knie-TEP geplant', + fragestellung: 'Ist der Eingriff zum jetzigen Zeitpunkt notwendig?', + updated_at: '2026-02-12T10:00:00Z', +} + +export const mockCaseListResponse: CaseListResponse = { + items: [mockCase, mockCaseNoIcd], + total: 2, + page: 1, + per_page: 20, +} + +export const mockEmptyCaseList: CaseListResponse = { + items: [], + total: 0, + page: 1, + per_page: 20, +} + +// ---------- Dashboard ---------- + +export const mockDashboardKPIs: DashboardKPIs = { + total_cases: 142, + pending_icd: 18, + pending_coding: 7, + total_gutachten: 89, + fallgruppen: { + onko: 65, + ortho: 42, + kardio: 20, + neuro: 15, + }, +} + +export const mockWeeklyData: WeeklyDataPoint[] = [ + { kw: 1, erstberatungen: 12, unterlagen: 8, ablehnungen: 1, keine_rm: 2, gutachten: 5 }, + { kw: 2, erstberatungen: 15, unterlagen: 10, ablehnungen: 0, keine_rm: 3, gutachten: 7 }, + { kw: 3, erstberatungen: 10, unterlagen: 9, ablehnungen: 2, keine_rm: 1, gutachten: 6 }, + { kw: 4, erstberatungen: 18, unterlagen: 12, ablehnungen: 1, keine_rm: 4, gutachten: 8 }, + { kw: 5, erstberatungen: 14, unterlagen: 11, ablehnungen: 0, keine_rm: 2, gutachten: 9 }, + { kw: 6, erstberatungen: 16, unterlagen: 13, ablehnungen: 1, keine_rm: 1, gutachten: 10 }, +] + +export const mockDashboardResponse: DashboardResponse = { + kpis: mockDashboardKPIs, + weekly: mockWeeklyData, +} + +// ---------- Notifications ---------- + +export const mockNotification: Notification = { + id: 1, + notification_type: 'disclosure_request', + title: 'Neue Freigabe-Anfrage', + message: 'Anna Schmidt hat eine Freigabe für Fall 2026-06-onko-A123456789 beantragt.', + is_read: false, + created_at: '2026-02-26T09:15:00Z', +} + +export const mockNotificationRead: Notification = { + id: 2, + notification_type: 'import_complete', + title: 'Import abgeschlossen', + message: '15 Fälle wurden erfolgreich importiert.', + is_read: true, + created_at: '2026-02-25T16:00:00Z', +} + +export const mockNotificationList: NotificationList = { + items: [mockNotification, mockNotificationRead], + unread_count: 1, +} + +// ---------- Reports ---------- + +export const mockReportMeta: ReportMeta = { + id: 1, + jahr: 2026, + kw: 6, + report_date: '2026-02-09', + generated_at: '2026-02-10T08:30:00Z', +} + +// ---------- Admin Users ---------- + +export const mockUserResponse: UserResponse = { + id: 1, + username: 'admin_user', + email: 'admin@dak.de', + first_name: 'Max', + last_name: 'Mustermann', + display_name: 'Max Mustermann', + avatar_url: null, + role: 'admin', + is_active: true, + mfa_enabled: false, + last_login: '2026-02-25T10:30:00Z', + created_at: '2026-01-01T08:00:00Z', +} + +// ---------- Audit Log ---------- + +export const mockAuditLogEntry: AuditLogEntry = { + id: 1, + user_id: 1, + action: 'case.update', + entity_type: 'case', + entity_id: '1', + old_values: { icd: null }, + new_values: { icd: 'C50.4' }, + ip_address: '10.10.181.104', + created_at: '2026-02-26T11:00:00Z', +} + +// ---------- Disclosure Requests ---------- + +export const mockDisclosureRequest: DisclosureRequest = { + id: 1, + case_id: 1, + requester_id: 2, + requester_username: 'dak_user', + fall_id: '2026-06-onko-A123456789', + reason: 'Benötige vollständige Patientendaten für Rückruf.', + status: 'pending', + reviewed_by: null, + reviewed_at: null, + expires_at: null, + created_at: '2026-02-26T09:00:00Z', +} + +export const mockDisclosureApproved: DisclosureRequest = { + ...mockDisclosureRequest, + id: 2, + status: 'approved', + reviewed_by: 1, + reviewed_at: '2026-02-26T09:30:00Z', + expires_at: '2026-02-27T09:30:00Z', +} diff --git a/frontend/src/test/mocks/handlers.ts b/frontend/src/test/mocks/handlers.ts new file mode 100644 index 0000000..2cbe578 --- /dev/null +++ b/frontend/src/test/mocks/handlers.ts @@ -0,0 +1,216 @@ +import { http, HttpResponse } from 'msw' +import { + mockTokenResponse, + mockUser, + mockCase, + mockCaseNoIcd, + mockCaseListResponse, + mockDashboardResponse, + mockNotificationList, + mockReportMeta, + mockUserResponse, + mockAuditLogEntry, + mockDisclosureRequest, + mockDisclosureApproved, +} from './data' + +export const handlers = [ + // ==================== Auth ==================== + + // POST /api/auth/login + http.post('/api/auth/login', () => { + return HttpResponse.json(mockTokenResponse) + }), + + // POST /api/auth/register + http.post('/api/auth/register', () => { + return HttpResponse.json({ user: mockUser }) + }), + + // POST /api/auth/logout + http.post('/api/auth/logout', () => { + return new HttpResponse(null, { status: 204 }) + }), + + // GET /api/auth/me + http.get('/api/auth/me', () => { + return HttpResponse.json(mockUser) + }), + + // POST /api/auth/refresh + http.post('/api/auth/refresh', () => { + return HttpResponse.json({ + access_token: 'refreshed-access-token', + refresh_token: 'refreshed-refresh-token', + token_type: 'bearer', + }) + }), + + // PUT /api/auth/profile + http.put('/api/auth/profile', () => { + return HttpResponse.json(mockUser) + }), + + // POST /api/auth/avatar + http.post('/api/auth/avatar', () => { + return HttpResponse.json({ ...mockUser, avatar_url: '/uploads/avatar-1.png' }) + }), + + // DELETE /api/auth/avatar + http.delete('/api/auth/avatar', () => { + return HttpResponse.json({ ...mockUser, avatar_url: null }) + }), + + // POST /api/auth/change-password + http.post('/api/auth/change-password', () => { + return new HttpResponse(null, { status: 200 }) + }), + + // POST /api/auth/mfa/setup + http.post('/api/auth/mfa/setup', () => { + return HttpResponse.json({ + secret: 'JBSWY3DPEHPK3PXP', + qr_uri: 'otpauth://totp/DAK:admin@dak.de?secret=JBSWY3DPEHPK3PXP&issuer=DAK', + }) + }), + + // POST /api/auth/mfa/verify + http.post('/api/auth/mfa/verify', () => { + return new HttpResponse(null, { status: 200 }) + }), + + // DELETE /api/auth/mfa + http.delete('/api/auth/mfa', () => { + return new HttpResponse(null, { status: 200 }) + }), + + // ==================== Cases ==================== + + // GET /api/cases/ + http.get('/api/cases/', () => { + return HttpResponse.json(mockCaseListResponse) + }), + + // GET /api/cases/pending-icd + http.get('/api/cases/pending-icd', () => { + return HttpResponse.json({ + items: [mockCaseNoIcd], + total: 1, + page: 1, + per_page: 20, + }) + }), + + // PUT /api/cases/:id + http.put('/api/cases/:id', ({ params }) => { + const id = Number(params.id) + return HttpResponse.json({ ...mockCase, id }) + }), + + // PUT /api/cases/:id/kvnr + http.put('/api/cases/:id/kvnr', ({ params }) => { + const id = Number(params.id) + return HttpResponse.json({ ...mockCase, id, kvnr: 'C111222333' }) + }), + + // PUT /api/cases/:id/icd + http.put('/api/cases/:id/icd', ({ params }) => { + const id = Number(params.id) + return HttpResponse.json({ ...mockCase, id, icd: 'C61' }) + }), + + // POST /api/cases/:caseId/disclosure-request + http.post('/api/cases/:caseId/disclosure-request', () => { + return HttpResponse.json(mockDisclosureRequest) + }), + + // ==================== Dashboard ==================== + + // GET /api/reports/dashboard + http.get('/api/reports/dashboard', () => { + return HttpResponse.json(mockDashboardResponse) + }), + + // ==================== Reports ==================== + + // GET /api/reports/list + http.get('/api/reports/list', () => { + return HttpResponse.json({ + items: [mockReportMeta], + total: 1, + }) + }), + + // POST /api/reports/generate + http.post('/api/reports/generate', () => { + return HttpResponse.json(mockReportMeta) + }), + + // DELETE /api/reports/delete + http.delete('/api/reports/delete', () => { + return HttpResponse.json({ deleted: 1 }) + }), + + // ==================== Notifications ==================== + + // GET /api/notifications + http.get('/api/notifications', () => { + return HttpResponse.json(mockNotificationList) + }), + + // PUT /api/notifications/:id/read + http.put('/api/notifications/:id/read', ({ params }) => { + const id = Number(params.id) + return HttpResponse.json({ + ...mockNotificationList.items[0], + id, + is_read: true, + }) + }), + + // PUT /api/notifications/read-all + http.put('/api/notifications/read-all', () => { + return HttpResponse.json({ marked_read: 1 }) + }), + + // ==================== Admin: Users ==================== + + // GET /api/admin/users + http.get('/api/admin/users', () => { + return HttpResponse.json([mockUserResponse]) + }), + + // POST /api/admin/users + http.post('/api/admin/users', () => { + return HttpResponse.json(mockUserResponse) + }), + + // PUT /api/admin/users/:id + http.put('/api/admin/users/:id', () => { + return HttpResponse.json(mockUserResponse) + }), + + // ==================== Admin: Audit Log ==================== + + // GET /api/admin/audit-log + http.get('/api/admin/audit-log', () => { + return HttpResponse.json([mockAuditLogEntry]) + }), + + // ==================== Admin: Disclosure Requests ==================== + + // GET /api/admin/disclosure-requests + http.get('/api/admin/disclosure-requests', () => { + return HttpResponse.json([mockDisclosureRequest]) + }), + + // GET /api/admin/disclosure-requests/count + http.get('/api/admin/disclosure-requests/count', () => { + return HttpResponse.json({ pending_count: 1 }) + }), + + // PUT /api/admin/disclosure-requests/:id + http.put('/api/admin/disclosure-requests/:id', () => { + return HttpResponse.json(mockDisclosureApproved) + }), +] diff --git a/frontend/src/test/mocks/server.ts b/frontend/src/test/mocks/server.ts new file mode 100644 index 0000000..86f7d61 --- /dev/null +++ b/frontend/src/test/mocks/server.ts @@ -0,0 +1,4 @@ +import { setupServer } from 'msw/node' +import { handlers } from './handlers' + +export const server = setupServer(...handlers) diff --git a/frontend/src/test/setup.ts b/frontend/src/test/setup.ts new file mode 100644 index 0000000..08228ff --- /dev/null +++ b/frontend/src/test/setup.ts @@ -0,0 +1,13 @@ +import '@testing-library/jest-dom/vitest' +import { cleanup } from '@testing-library/react' +import { afterAll, afterEach, beforeAll } from 'vitest' +import { server } from './mocks/server' + +beforeAll(() => server.listen({ onUnhandledRequest: 'error' })) + +afterEach(() => { + cleanup() + server.resetHandlers() +}) + +afterAll(() => server.close())