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:
307
internal/handler/upload_handler_test.go
Normal file
307
internal/handler/upload_handler_test.go
Normal file
@@ -0,0 +1,307 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gcy_hpc_server/internal/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type mockUploadService struct {
|
||||
initUploadFn func(ctx context.Context, req model.InitUploadRequest) (interface{}, error)
|
||||
uploadChunkFn func(ctx context.Context, sessionID int64, chunkIndex int, reader io.Reader, size int64) error
|
||||
completeUploadFn func(ctx context.Context, sessionID int64) (*model.FileResponse, error)
|
||||
getUploadStatusFn func(ctx context.Context, sessionID int64) (*model.UploadSessionResponse, error)
|
||||
cancelUploadFn func(ctx context.Context, sessionID int64) error
|
||||
}
|
||||
|
||||
func (m *mockUploadService) InitUpload(ctx context.Context, req model.InitUploadRequest) (interface{}, error) {
|
||||
return m.initUploadFn(ctx, req)
|
||||
}
|
||||
|
||||
func (m *mockUploadService) UploadChunk(ctx context.Context, sessionID int64, chunkIndex int, reader io.Reader, size int64) error {
|
||||
return m.uploadChunkFn(ctx, sessionID, chunkIndex, reader, size)
|
||||
}
|
||||
|
||||
func (m *mockUploadService) CompleteUpload(ctx context.Context, sessionID int64) (*model.FileResponse, error) {
|
||||
return m.completeUploadFn(ctx, sessionID)
|
||||
}
|
||||
|
||||
func (m *mockUploadService) GetUploadStatus(ctx context.Context, sessionID int64) (*model.UploadSessionResponse, error) {
|
||||
return m.getUploadStatusFn(ctx, sessionID)
|
||||
}
|
||||
|
||||
func (m *mockUploadService) CancelUpload(ctx context.Context, sessionID int64) error {
|
||||
return m.cancelUploadFn(ctx, sessionID)
|
||||
}
|
||||
|
||||
func setupUploadHandler(mock uploadServiceProvider) (*UploadHandler, *gin.Engine) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
h := &UploadHandler{svc: mock, logger: zap.NewNop()}
|
||||
r := gin.New()
|
||||
v1 := r.Group("/api/v1")
|
||||
uploads := v1.Group("/files/uploads")
|
||||
uploads.POST("", h.InitUpload)
|
||||
uploads.PUT("/:id/chunks/:index", h.UploadChunk)
|
||||
uploads.POST("/:id/complete", h.CompleteUpload)
|
||||
uploads.GET("/:id", h.GetUploadStatus)
|
||||
uploads.DELETE("/:id", h.CancelUpload)
|
||||
return h, r
|
||||
}
|
||||
|
||||
func TestInitUpload_NewSession(t *testing.T) {
|
||||
mock := &mockUploadService{
|
||||
initUploadFn: func(ctx context.Context, req model.InitUploadRequest) (interface{}, error) {
|
||||
return model.UploadSessionResponse{
|
||||
ID: 1,
|
||||
FileName: req.FileName,
|
||||
FileSize: req.FileSize,
|
||||
ChunkSize: 5 * 1024 * 1024,
|
||||
TotalChunks: 2,
|
||||
SHA256: req.SHA256,
|
||||
Status: "pending",
|
||||
UploadedChunks: []int{},
|
||||
ExpiresAt: time.Now().Add(24 * time.Hour),
|
||||
CreatedAt: time.Now(),
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
_, r := setupUploadHandler(mock)
|
||||
|
||||
body, _ := json.Marshal(model.InitUploadRequest{
|
||||
FileName: "test.bin",
|
||||
FileSize: 10 * 1024 * 1024,
|
||||
SHA256: "abc123",
|
||||
})
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest(http.MethodPost, "/api/v1/files/uploads", 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["status"] != "pending" {
|
||||
t.Fatalf("expected status=pending, got %v", data["status"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitUpload_DedupHit(t *testing.T) {
|
||||
mock := &mockUploadService{
|
||||
initUploadFn: func(ctx context.Context, req model.InitUploadRequest) (interface{}, error) {
|
||||
return model.FileResponse{
|
||||
ID: 42,
|
||||
Name: req.FileName,
|
||||
Size: req.FileSize,
|
||||
SHA256: req.SHA256,
|
||||
MimeType: "application/octet-stream",
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
_, r := setupUploadHandler(mock)
|
||||
|
||||
body, _ := json.Marshal(model.InitUploadRequest{
|
||||
FileName: "existing.bin",
|
||||
FileSize: 1024,
|
||||
SHA256: "existinghash",
|
||||
})
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest(http.MethodPost, "/api/v1/files/uploads", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
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"].(map[string]interface{})
|
||||
if data["sha256"] != "existinghash" {
|
||||
t.Fatalf("expected sha256=existinghash, got %v", data["sha256"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitUpload_MissingFields(t *testing.T) {
|
||||
mock := &mockUploadService{
|
||||
initUploadFn: func(ctx context.Context, req model.InitUploadRequest) (interface{}, error) {
|
||||
return nil, nil
|
||||
},
|
||||
}
|
||||
_, r := setupUploadHandler(mock)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest(http.MethodPost, "/api/v1/files/uploads", bytes.NewReader([]byte(`{}`)))
|
||||
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 TestUploadChunk_Success(t *testing.T) {
|
||||
var receivedSessionID int64
|
||||
var receivedChunkIndex int
|
||||
|
||||
mock := &mockUploadService{
|
||||
uploadChunkFn: func(ctx context.Context, sessionID int64, chunkIndex int, reader io.Reader, size int64) error {
|
||||
receivedSessionID = sessionID
|
||||
receivedChunkIndex = chunkIndex
|
||||
readBytes, _ := io.ReadAll(reader)
|
||||
if len(readBytes) == 0 {
|
||||
t.Error("expected non-empty chunk data")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
_, r := setupUploadHandler(mock)
|
||||
|
||||
var buf bytes.Buffer
|
||||
writer := multipart.NewWriter(&buf)
|
||||
part, _ := writer.CreateFormFile("chunk", "test.bin")
|
||||
fmt.Fprintf(part, "chunk data here")
|
||||
writer.Close()
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest(http.MethodPut, "/api/v1/files/uploads/1/chunks/0", &buf)
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
if receivedSessionID != 1 {
|
||||
t.Fatalf("expected session_id=1, got %d", receivedSessionID)
|
||||
}
|
||||
if receivedChunkIndex != 0 {
|
||||
t.Fatalf("expected chunk_index=0, got %d", receivedChunkIndex)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompleteUpload_Success(t *testing.T) {
|
||||
mock := &mockUploadService{
|
||||
completeUploadFn: func(ctx context.Context, sessionID int64) (*model.FileResponse, error) {
|
||||
return &model.FileResponse{
|
||||
ID: 10,
|
||||
Name: "completed.bin",
|
||||
Size: 2048,
|
||||
SHA256: "completehash",
|
||||
MimeType: "application/octet-stream",
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
_, r := setupUploadHandler(mock)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest(http.MethodPost, "/api/v1/files/uploads/5/complete", nil)
|
||||
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"] != "completed.bin" {
|
||||
t.Fatalf("expected name=completed.bin, got %v", data["name"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetUploadStatus_Success(t *testing.T) {
|
||||
mock := &mockUploadService{
|
||||
getUploadStatusFn: func(ctx context.Context, sessionID int64) (*model.UploadSessionResponse, error) {
|
||||
return &model.UploadSessionResponse{
|
||||
ID: sessionID,
|
||||
FileName: "status.bin",
|
||||
FileSize: 4096,
|
||||
ChunkSize: 2048,
|
||||
TotalChunks: 2,
|
||||
SHA256: "statushash",
|
||||
Status: "uploading",
|
||||
UploadedChunks: []int{0},
|
||||
ExpiresAt: time.Now().Add(24 * time.Hour),
|
||||
CreatedAt: time.Now(),
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
_, r := setupUploadHandler(mock)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest(http.MethodGet, "/api/v1/files/uploads/3", 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"].(map[string]interface{})
|
||||
if data["status"] != "uploading" {
|
||||
t.Fatalf("expected status=uploading, got %v", data["status"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestCancelUpload_Success(t *testing.T) {
|
||||
var receivedSessionID int64
|
||||
|
||||
mock := &mockUploadService{
|
||||
cancelUploadFn: func(ctx context.Context, sessionID int64) error {
|
||||
receivedSessionID = sessionID
|
||||
return nil
|
||||
},
|
||||
}
|
||||
_, r := setupUploadHandler(mock)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest(http.MethodDelete, "/api/v1/files/uploads/7", nil)
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
if receivedSessionID != 7 {
|
||||
t.Fatalf("expected session_id=7, got %d", receivedSessionID)
|
||||
}
|
||||
|
||||
var resp map[string]interface{}
|
||||
json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
data := resp["data"].(map[string]interface{})
|
||||
if data["message"] != "upload cancelled" {
|
||||
t.Fatalf("expected message='upload cancelled', got %v", data["message"])
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user