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>
308 lines
8.9 KiB
Go
308 lines
8.9 KiB
Go
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"])
|
|
}
|
|
}
|