Enhance file upload functionality to support multiple files; update related tests and response structure

This commit is contained in:
2025-07-12 14:58:32 +02:00
parent 491d056dd4
commit 9f01c64e5e
3 changed files with 81 additions and 21 deletions

View File

@@ -24,6 +24,7 @@ type SaveFileResponse struct {
UpdatedAt time.Time `json:"updatedAt"` UpdatedAt time.Time `json:"updatedAt"`
} }
// UploadFilesResponse represents a response to an upload files request
type UploadFilesResponse struct { type UploadFilesResponse struct {
FilePaths []string `json:"filePaths"` FilePaths []string `json:"filePaths"`
} }
@@ -330,6 +331,16 @@ func (h *Handler) UploadFile() http.HandlerFunc {
"clientIP", r.RemoteAddr, "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 form := r.MultipartForm
if form == nil || len(form.File) == 0 { if form == nil || len(form.File) == 0 {
log.Debug("no files found in form") log.Debug("no files found in form")
@@ -338,12 +349,6 @@ func (h *Handler) UploadFile() http.HandlerFunc {
} }
uploadPath := r.URL.Query().Get("file_path") 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) decodedPath, err := url.PathUnescape(uploadPath)
if err != nil { if err != nil {
log.Error("failed to decode file path", log.Error("failed to decode file path",

View File

@@ -241,23 +241,22 @@ func testFileHandlers(t *testing.T, dbConfig DatabaseConfig) {
}) })
t.Run("upload file", func(t *testing.T) { 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" fileName := "uploaded-test.txt"
fileContent := "This is an uploaded file" 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) require.Equal(t, http.StatusOK, rr.Code)
// Verify response structure // Verify response structure for multiple files API
var response struct { var response struct {
FilePath string `json:"filePath"` FilePaths []string `json:"filePaths"`
Size int64 `json:"size"`
UpdatedAt string `json:"updatedAt"`
} }
err := json.NewDecoder(rr.Body).Decode(&response) err := json.NewDecoder(rr.Body).Decode(&response)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, "uploads/"+fileName, response.FilePath) require.Len(t, response.FilePaths, 1)
assert.Equal(t, int64(len(fileContent)), response.Size) assert.Equal(t, "uploads/"+fileName, response.FilePaths[0])
// Verify file was saved // Verify file was saved
rr = h.makeRequest(t, http.MethodGet, baseURL+"/content?file_path="+url.QueryEscape("uploads/"+fileName), nil, h.RegularTestUser) 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()) 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) { 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) assert.Equal(t, http.StatusBadRequest, rr.Code)
}) })
}) })

View File

@@ -328,18 +328,20 @@ func (h *testHarness) makeRequestRaw(t *testing.T, method, path string, body io.
return h.executeRequest(req) return h.executeRequest(req)
} }
// makeUploadRequest creates a multipart form request for file uploads // makeUploadRequest creates a multipart form request for file uploads (single or multiple)
// If fileName is empty, creates an empty multipart form without a file // For single file: use map with one entry, e.g., map[string]string{"file.txt": "content"}
func (h *testHarness) makeUploadRequest(t *testing.T, path, fileName, fileContent string, testUser *testUser) *httptest.ResponseRecorder { // 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() t.Helper()
// Create multipart form // Create multipart form
var buf bytes.Buffer var buf bytes.Buffer
writer := multipart.NewWriter(&buf) writer := multipart.NewWriter(&buf)
// Only add file part if fileName is not empty // Add all files
if fileName != "" { for fileName, fileContent := range files {
part, err := writer.CreateFormFile("file", fileName) part, err := writer.CreateFormFile("files", fileName)
if err != nil { if err != nil {
t.Fatalf("Failed to create form file: %v", err) t.Fatalf("Failed to create form file: %v", err)
} }