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"]) } }