From 9f01c64e5e829f715ac42148bcb6fdbcb85b9d73 Mon Sep 17 00:00:00 2001 From: LordMathis Date: Sat, 12 Jul 2025 14:58:32 +0200 Subject: [PATCH] Enhance file upload functionality to support multiple files; update related tests and response structure --- server/internal/handlers/file_handlers.go | 17 +++-- .../file_handlers_integration_test.go | 71 ++++++++++++++++--- server/internal/handlers/integration_test.go | 14 ++-- 3 files changed, 81 insertions(+), 21 deletions(-) diff --git a/server/internal/handlers/file_handlers.go b/server/internal/handlers/file_handlers.go index a2ecb51..dfbbbd8 100644 --- a/server/internal/handlers/file_handlers.go +++ b/server/internal/handlers/file_handlers.go @@ -24,6 +24,7 @@ type SaveFileResponse struct { UpdatedAt time.Time `json:"updatedAt"` } +// UploadFilesResponse represents a response to an upload files request type UploadFilesResponse struct { FilePaths []string `json:"filePaths"` } @@ -330,6 +331,16 @@ func (h *Handler) UploadFile() http.HandlerFunc { "clientIP", r.RemoteAddr, ) + // Parse multipart form (max 32MB in memory) + err := r.ParseMultipartForm(32 << 20) + if err != nil { + log.Error("failed to parse multipart form", + "error", err.Error(), + ) + respondError(w, "Failed to parse form", http.StatusBadRequest) + return + } + form := r.MultipartForm if form == nil || len(form.File) == 0 { log.Debug("no files found in form") @@ -338,12 +349,6 @@ func (h *Handler) UploadFile() http.HandlerFunc { } uploadPath := r.URL.Query().Get("file_path") - if uploadPath == "" { - log.Debug("missing file_path parameter") - respondError(w, "file_path is required", http.StatusBadRequest) - return - } - decodedPath, err := url.PathUnescape(uploadPath) if err != nil { log.Error("failed to decode file path", diff --git a/server/internal/handlers/file_handlers_integration_test.go b/server/internal/handlers/file_handlers_integration_test.go index 3d64e40..116e612 100644 --- a/server/internal/handlers/file_handlers_integration_test.go +++ b/server/internal/handlers/file_handlers_integration_test.go @@ -241,23 +241,22 @@ func testFileHandlers(t *testing.T, dbConfig DatabaseConfig) { }) t.Run("upload file", func(t *testing.T) { - t.Run("successful upload", func(t *testing.T) { + t.Run("successful single file upload", func(t *testing.T) { fileName := "uploaded-test.txt" fileContent := "This is an uploaded file" - rr := h.makeUploadRequest(t, baseURL+"/upload?file_path="+url.QueryEscape("uploads"), fileName, fileContent, h.RegularTestUser) + files := map[string]string{fileName: fileContent} + rr := h.makeUploadRequest(t, baseURL+"/upload?file_path="+url.QueryEscape("uploads"), files, h.RegularTestUser) require.Equal(t, http.StatusOK, rr.Code) - // Verify response structure + // Verify response structure for multiple files API var response struct { - FilePath string `json:"filePath"` - Size int64 `json:"size"` - UpdatedAt string `json:"updatedAt"` + FilePaths []string `json:"filePaths"` } err := json.NewDecoder(rr.Body).Decode(&response) require.NoError(t, err) - assert.Equal(t, "uploads/"+fileName, response.FilePath) - assert.Equal(t, int64(len(fileContent)), response.Size) + require.Len(t, response.FilePaths, 1) + assert.Equal(t, "uploads/"+fileName, response.FilePaths[0]) // Verify file was saved rr = h.makeRequest(t, http.MethodGet, baseURL+"/content?file_path="+url.QueryEscape("uploads/"+fileName), nil, h.RegularTestUser) @@ -265,8 +264,62 @@ func testFileHandlers(t *testing.T, dbConfig DatabaseConfig) { assert.Equal(t, fileContent, rr.Body.String()) }) + t.Run("successful multiple files upload", func(t *testing.T) { + files := map[string]string{ + "file1.txt": "Content of first file", + "file2.md": "# Content of second file", + "file3.py": "print('Content of third file')", + } + + rr := h.makeUploadRequest(t, baseURL+"/upload?file_path="+url.QueryEscape("batch"), files, h.RegularTestUser) + require.Equal(t, http.StatusOK, rr.Code) + + // Verify response structure + var response struct { + FilePaths []string `json:"filePaths"` + } + err := json.NewDecoder(rr.Body).Decode(&response) + require.NoError(t, err) + require.Len(t, response.FilePaths, 3) + + // Verify all files were saved with correct paths + expectedPaths := []string{"batch/file1.txt", "batch/file2.md", "batch/file3.py"} + for _, expectedPath := range expectedPaths { + assert.Contains(t, response.FilePaths, expectedPath) + } + + // Verify file contents + for fileName, expectedContent := range files { + filePath := "batch/" + fileName + rr = h.makeRequest(t, http.MethodGet, baseURL+"/content?file_path="+url.QueryEscape(filePath), nil, h.RegularTestUser) + require.Equal(t, http.StatusOK, rr.Code) + assert.Equal(t, expectedContent, rr.Body.String()) + } + }) + t.Run("upload without file", func(t *testing.T) { - rr := h.makeUploadRequest(t, baseURL+"/upload?file_path="+url.QueryEscape("test"), "", "", h.RegularTestUser) + // Empty map means no files + files := map[string]string{} + rr := h.makeUploadRequest(t, baseURL+"/upload?file_path="+url.QueryEscape("test"), files, h.RegularTestUser) + assert.Equal(t, http.StatusBadRequest, rr.Code) + }) + + t.Run("upload with missing file_path parameter", func(t *testing.T) { + fileName := "test.txt" + fileContent := "test content" + files := map[string]string{fileName: fileContent} + + rr := h.makeUploadRequest(t, baseURL+"/upload", files, h.RegularTestUser) + assert.Equal(t, http.StatusBadRequest, rr.Code) + }) + + t.Run("upload with invalid file_path", func(t *testing.T) { + fileName := "test.txt" + fileContent := "test content" + invalidPath := "../../../etc/passwd" + files := map[string]string{fileName: fileContent} + + rr := h.makeUploadRequest(t, baseURL+"/upload?file_path="+url.QueryEscape(invalidPath), files, h.RegularTestUser) assert.Equal(t, http.StatusBadRequest, rr.Code) }) }) diff --git a/server/internal/handlers/integration_test.go b/server/internal/handlers/integration_test.go index 2e0acc5..64efca0 100644 --- a/server/internal/handlers/integration_test.go +++ b/server/internal/handlers/integration_test.go @@ -328,18 +328,20 @@ func (h *testHarness) makeRequestRaw(t *testing.T, method, path string, body io. return h.executeRequest(req) } -// makeUploadRequest creates a multipart form request for file uploads -// If fileName is empty, creates an empty multipart form without a file -func (h *testHarness) makeUploadRequest(t *testing.T, path, fileName, fileContent string, testUser *testUser) *httptest.ResponseRecorder { +// makeUploadRequest creates a multipart form request for file uploads (single or multiple) +// For single file: use map with one entry, e.g., map[string]string{"file.txt": "content"} +// For multiple files: use map with multiple entries +// For empty form (no files): pass empty map +func (h *testHarness) makeUploadRequest(t *testing.T, path string, files map[string]string, testUser *testUser) *httptest.ResponseRecorder { t.Helper() // Create multipart form var buf bytes.Buffer writer := multipart.NewWriter(&buf) - // Only add file part if fileName is not empty - if fileName != "" { - part, err := writer.CreateFormFile("file", fileName) + // Add all files + for fileName, fileContent := range files { + part, err := writer.CreateFormFile("files", fileName) if err != nil { t.Fatalf("Failed to create form file: %v", err) }