diff --git a/frontend/src/__tests__/dms/FolderCreate.validation.test.tsx b/frontend/src/__tests__/dms/FolderCreate.validation.test.tsx new file mode 100644 index 0000000..a81cd3e --- /dev/null +++ b/frontend/src/__tests__/dms/FolderCreate.validation.test.tsx @@ -0,0 +1,69 @@ +import React from 'react'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; + +vi.mock('@/api/dms', () => ({ + shareFile: vi.fn().mockResolvedValue({}), + removeShare: vi.fn(), +})); + +vi.mock('@/api/permissions', () => ({ + fetchFilePermissions: vi.fn().mockResolvedValue([]), + grantPermission: vi.fn(), + revokePermission: vi.fn(), + createShareLink: vi.fn(), + revokeShareLink: vi.fn(), +})); + +vi.mock('@/components/ui/Toast', () => ({ + useToast: () => ({ success: vi.fn(), error: vi.fn(), info: vi.fn(), warning: vi.fn() }), +})); + +import { ShareDialog } from '@/components/dms/ShareDialog'; +import { shareFile } from '@/api/dms'; + +beforeEach(() => { + vi.clearAllMocks(); +}); + +describe('ShareDialog validation (RHF + Zod)', () => { + it('shows validation error when share ID is empty', async () => { + render( + + ); + // Find the submit button inside the add-share form + const addShareButtons = screen.getAllByRole('button', { name: /hinzufügen|add share/i }); + const submitBtn = addShareButtons.find((b) => b.getAttribute('type') === 'submit'); + expect(submitBtn).toBeTruthy(); + fireEvent.click(submitBtn!); + await waitFor(() => { + const errors = screen.getAllByText(/erforderlich|required/i); + expect(errors.length).toBeGreaterThan(0); + }); + }); + + it('calls shareFile when form is valid', async () => { + render( + + ); + // Fill in share ID - find the input inside the share-add-section + const shareSection = screen.getByTestId('share-add-section'); + const input = shareSection.querySelector('input'); + expect(input).toBeTruthy(); + fireEvent.change(input!, { target: { value: 'user-123' } }); + fireEvent.submit(input!.closest('form')!); + await waitFor(() => { + expect(shareFile).toHaveBeenCalledWith('f1', expect.objectContaining({ user_id: 'user-123' })); + }, { timeout: 3000 }); + }); +}); diff --git a/frontend/src/components/dms/ShareDialog.tsx b/frontend/src/components/dms/ShareDialog.tsx index 580b85c..33f6c17 100644 --- a/frontend/src/components/dms/ShareDialog.tsx +++ b/frontend/src/components/dms/ShareDialog.tsx @@ -5,6 +5,9 @@ import React, { useState, useEffect, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { z } from 'zod'; import { Modal } from '@/components/ui/Modal'; import { Button } from '@/components/ui/Button'; import { Input } from '@/components/ui/Input'; @@ -45,12 +48,22 @@ export function ShareDialog({ open, file, onClose, onShared }: ShareDialogProps) const [loading, setLoading] = useState(false); const [shareType, setShareType] = useState<'user' | 'group'>('user'); - const [shareId, setShareId] = useState(''); const [sharePermission, setSharePermission] = useState<'read' | 'write'>('read'); const [linkPassword, setLinkPassword] = useState(''); const [linkExpiry, setLinkExpiry] = useState(''); const [submitting, setSubmitting] = useState(false); + // ── Share form (RHF + Zod) ── + const shareSchema = z.object({ + shareId: z.string().min(1, 'required'), + }); + type ShareFormData = z.infer; + + const { register: registerShare, handleSubmit: handleSubmitShare, reset: resetShare, formState: { errors: shareErrors } } = useForm({ + resolver: zodResolver(shareSchema), + defaultValues: { shareId: '' }, + }); + const loadData = useCallback(async () => { if (!file) return; setLoading(true); @@ -69,18 +82,18 @@ export function ShareDialog({ open, file, onClose, onShared }: ShareDialogProps) } }, [open, file, loadData]); - const handleAddShare = useCallback(async () => { - if (!file || !shareId) return; + const handleAddShare = useCallback(async (data: ShareFormData) => { + if (!file) return; setSubmitting(true); try { const payload: { user_id?: string; group_id?: string; permission: 'read' | 'write' } = { permission: sharePermission, }; - if (shareType === 'user') payload.user_id = shareId; - else payload.group_id = shareId; + if (shareType === 'user') payload.user_id = data.shareId; + else payload.group_id = data.shareId; await shareFile(file.id, payload); toast.success(t('dms.shared')); - setShareId(''); + resetShare({ shareId: '' }); onShared(); loadData(); } catch (err) { @@ -88,7 +101,7 @@ export function ShareDialog({ open, file, onClose, onShared }: ShareDialogProps) toast.error(msg); } setSubmitting(false); - }, [file, shareId, shareType, sharePermission, toast, t, onShared, loadData]); + }, [file, shareType, sharePermission, toast, t, onShared, loadData, resetShare]); const handleRemovePermission = useCallback(async (userId: string) => { if (!file) return; @@ -155,6 +168,7 @@ export function ShareDialog({ open, file, onClose, onShared }: ShareDialogProps) {/* Add share section */}

{t('dms.addShare')}

+
setShareId(e.target.value)} + {...registerShare('shareId')} + error={shareErrors.shareId?.message === 'required' ? t('validation.required') : undefined} placeholder={shareType === 'user' ? t('dms.userId') : t('dms.groupId')} /> setNewFolderName(e.target.value)} + {...registerFolder('name')} + error={folderErrors.name?.message === 'required' ? t('validation.required') : undefined} placeholder={t('dms.folderName')} - onKeyDown={(e) => { - if (e.key === 'Enter') handleCreateFolder(); - }} /> - -
-
+ )} {/* Upload dropzone */}