feat(handler): add upload, file, and folder handlers with routes

Add UploadHandler (5 endpoints), FileHandler (4 endpoints), FolderHandler (4 endpoints) with Gin route registration in server.go.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
dailz
2026-04-15 09:23:17 +08:00
parent f0847d3978
commit 2298e92516
7 changed files with 1376 additions and 1 deletions

View File

@@ -0,0 +1,206 @@
package handler
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"gcy_hpc_server/internal/model"
"gcy_hpc_server/internal/service"
"gcy_hpc_server/internal/store"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
gormlogger "gorm.io/gorm/logger"
)
func setupFolderHandler() (*FolderHandler, *gorm.DB) {
db, _ := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{Logger: gormlogger.Default.LogMode(gormlogger.Silent)})
db.AutoMigrate(&model.Folder{}, &model.File{})
folderStore := store.NewFolderStore(db)
fileStore := store.NewFileStore(db)
svc := service.NewFolderService(folderStore, fileStore, zap.NewNop())
h := NewFolderHandler(svc, zap.NewNop())
return h, db
}
func setupFolderRouter(h *FolderHandler) *gin.Engine {
gin.SetMode(gin.TestMode)
r := gin.New()
v1 := r.Group("/api/v1")
folders := v1.Group("/files/folders")
folders.POST("", h.CreateFolder)
folders.GET("/:id", h.GetFolder)
folders.GET("", h.ListFolders)
folders.DELETE("/:id", h.DeleteFolder)
return r
}
func createTestFolder(h *FolderHandler, r *gin.Engine) int64 {
body, _ := json.Marshal(model.CreateFolderRequest{Name: "test-folder"})
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodPost, "/api/v1/files/folders", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
r.ServeHTTP(w, req)
var resp map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &resp)
data := resp["data"].(map[string]interface{})
return int64(data["id"].(float64))
}
func TestCreateFolder_Success(t *testing.T) {
h, _ := setupFolderHandler()
r := setupFolderRouter(h)
body, _ := json.Marshal(model.CreateFolderRequest{Name: "my-folder"})
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodPost, "/api/v1/files/folders", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
r.ServeHTTP(w, req)
if w.Code != http.StatusCreated {
t.Fatalf("expected 201, got %d: %s", w.Code, w.Body.String())
}
var resp map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &resp)
if !resp["success"].(bool) {
t.Fatal("expected success=true")
}
data := resp["data"].(map[string]interface{})
if data["name"] != "my-folder" {
t.Fatalf("expected name=my-folder, got %v", data["name"])
}
if data["path"] != "/my-folder/" {
t.Fatalf("expected path=/my-folder/, got %v", data["path"])
}
}
func TestCreateFolder_PathTraversal(t *testing.T) {
h, _ := setupFolderHandler()
r := setupFolderRouter(h)
body, _ := json.Marshal(model.CreateFolderRequest{Name: ".."})
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodPost, "/api/v1/files/folders", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
r.ServeHTTP(w, req)
if w.Code != http.StatusBadRequest {
t.Fatalf("expected 400, got %d: %s", w.Code, w.Body.String())
}
}
func TestCreateFolder_MissingName(t *testing.T) {
h, _ := setupFolderHandler()
r := setupFolderRouter(h)
body, _ := json.Marshal(map[string]interface{}{})
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodPost, "/api/v1/files/folders", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
r.ServeHTTP(w, req)
if w.Code != http.StatusBadRequest {
t.Fatalf("expected 400, got %d: %s", w.Code, w.Body.String())
}
}
func TestGetFolder_Success(t *testing.T) {
h, _ := setupFolderHandler()
r := setupFolderRouter(h)
id := createTestFolder(h, r)
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/api/v1/files/folders/"+itoa(id), nil)
r.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
}
var resp map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &resp)
data := resp["data"].(map[string]interface{})
if data["name"] != "test-folder" {
t.Fatalf("expected name=test-folder, got %v", data["name"])
}
}
func TestGetFolder_NotFound(t *testing.T) {
h, _ := setupFolderHandler()
r := setupFolderRouter(h)
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/api/v1/files/folders/999", nil)
r.ServeHTTP(w, req)
if w.Code != http.StatusNotFound {
t.Fatalf("expected 404, got %d: %s", w.Code, w.Body.String())
}
}
func TestListFolders_Success(t *testing.T) {
h, _ := setupFolderHandler()
r := setupFolderRouter(h)
createTestFolder(h, r)
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/api/v1/files/folders", nil)
r.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
}
var resp map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &resp)
if !resp["success"].(bool) {
t.Fatal("expected success=true")
}
data := resp["data"].([]interface{})
if len(data) < 1 {
t.Fatal("expected at least 1 folder")
}
}
func TestDeleteFolder_Success(t *testing.T) {
h, _ := setupFolderHandler()
r := setupFolderRouter(h)
id := createTestFolder(h, r)
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodDelete, "/api/v1/files/folders/"+itoa(id), nil)
r.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
}
}
func TestDeleteFolder_NonEmpty(t *testing.T) {
h, db := setupFolderHandler()
r := setupFolderRouter(h)
parentID := createTestFolder(h, r)
child := &model.Folder{
Name: "child-folder",
ParentID: &parentID,
Path: "/test-folder/child-folder/",
}
db.Create(child)
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodDelete, "/api/v1/files/folders/"+itoa(parentID), nil)
r.ServeHTTP(w, req)
if w.Code != http.StatusBadRequest {
t.Fatalf("expected 400, got %d: %s", w.Code, w.Body.String())
}
}