package handler import ( "context" "encoding/json" "fmt" "io" "net/http" "net/http/httptest" "strings" "testing" "time" "gcy_hpc_server/internal/model" "github.com/gin-gonic/gin" "go.uber.org/zap" ) type mockFileService struct { listFilesFn func(ctx context.Context, folderID *int64, page, pageSize int, search string) ([]model.FileResponse, int64, error) getFileMetadataFn func(ctx context.Context, fileID int64) (*model.File, *model.FileBlob, error) downloadFileFn func(ctx context.Context, fileID int64, rangeHeader string) (io.ReadCloser, *model.File, *model.FileBlob, int64, int64, error) deleteFileFn func(ctx context.Context, fileID int64) error } func (m *mockFileService) ListFiles(ctx context.Context, folderID *int64, page, pageSize int, search string) ([]model.FileResponse, int64, error) { return m.listFilesFn(ctx, folderID, page, pageSize, search) } func (m *mockFileService) GetFileMetadata(ctx context.Context, fileID int64) (*model.File, *model.FileBlob, error) { return m.getFileMetadataFn(ctx, fileID) } func (m *mockFileService) DownloadFile(ctx context.Context, fileID int64, rangeHeader string) (io.ReadCloser, *model.File, *model.FileBlob, int64, int64, error) { return m.downloadFileFn(ctx, fileID, rangeHeader) } func (m *mockFileService) DeleteFile(ctx context.Context, fileID int64) error { return m.deleteFileFn(ctx, fileID) } type fileHandlerSetup struct { handler *FileHandler mock *mockFileService router *gin.Engine } func newFileHandlerSetup() *fileHandlerSetup { gin.SetMode(gin.TestMode) mock := &mockFileService{} h := &FileHandler{ svc: mock, logger: zap.NewNop(), } r := gin.New() v1 := r.Group("/api/v1") files := v1.Group("/files") files.GET("", h.ListFiles) files.GET("/:id", h.GetFile) files.GET("/:id/download", h.DownloadFile) files.DELETE("/:id", h.DeleteFile) return &fileHandlerSetup{handler: h, mock: mock, router: r} } // ---- ListFiles Tests ---- func TestListFiles_Empty(t *testing.T) { s := newFileHandlerSetup() s.mock.listFilesFn = func(ctx context.Context, folderID *int64, page, pageSize int, search string) ([]model.FileResponse, int64, error) { return []model.FileResponse{}, 0, nil } w := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodGet, "/api/v1/files", nil) s.router.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{}) files := data["files"].([]interface{}) if len(files) != 0 { t.Fatalf("expected empty files list, got %d", len(files)) } if data["total"].(float64) != 0 { t.Fatalf("expected total=0, got %v", data["total"]) } } func TestListFiles_WithFiles(t *testing.T) { s := newFileHandlerSetup() now := time.Now() s.mock.listFilesFn = func(ctx context.Context, folderID *int64, page, pageSize int, search string) ([]model.FileResponse, int64, error) { return []model.FileResponse{ {ID: 1, Name: "a.txt", Size: 100, MimeType: "text/plain", SHA256: "abc123", CreatedAt: now, UpdatedAt: now}, {ID: 2, Name: "b.pdf", Size: 200, MimeType: "application/pdf", SHA256: "def456", CreatedAt: now, UpdatedAt: now}, }, 2, nil } w := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodGet, "/api/v1/files?page=1&page_size=10", nil) s.router.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{}) files := data["files"].([]interface{}) if len(files) != 2 { t.Fatalf("expected 2 files, got %d", len(files)) } if data["total"].(float64) != 2 { t.Fatalf("expected total=2, got %v", data["total"]) } if data["page"].(float64) != 1 { t.Fatalf("expected page=1, got %v", data["page"]) } if data["page_size"].(float64) != 10 { t.Fatalf("expected page_size=10, got %v", data["page_size"]) } } func TestListFiles_WithFolderID(t *testing.T) { s := newFileHandlerSetup() var capturedFolderID *int64 s.mock.listFilesFn = func(ctx context.Context, folderID *int64, page, pageSize int, search string) ([]model.FileResponse, int64, error) { capturedFolderID = folderID return []model.FileResponse{}, 0, nil } w := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodGet, "/api/v1/files?folder_id=5", nil) s.router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) } if capturedFolderID == nil || *capturedFolderID != 5 { t.Fatalf("expected folder_id=5, got %v", capturedFolderID) } } func TestListFiles_ServiceError(t *testing.T) { s := newFileHandlerSetup() s.mock.listFilesFn = func(ctx context.Context, folderID *int64, page, pageSize int, search string) ([]model.FileResponse, int64, error) { return nil, 0, fmt.Errorf("db error") } w := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodGet, "/api/v1/files", nil) s.router.ServeHTTP(w, req) if w.Code != http.StatusInternalServerError { t.Fatalf("expected 500, got %d", w.Code) } } // ---- GetFile Tests ---- func TestGetFile_Found(t *testing.T) { s := newFileHandlerSetup() now := time.Now() s.mock.getFileMetadataFn = func(ctx context.Context, fileID int64) (*model.File, *model.FileBlob, error) { return &model.File{ ID: 1, Name: "test.txt", BlobSHA256: "abc123", CreatedAt: now, UpdatedAt: now, }, &model.FileBlob{ ID: 1, SHA256: "abc123", FileSize: 1024, MimeType: "text/plain", CreatedAt: now, }, nil } w := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodGet, "/api/v1/files/1", nil) s.router.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") } } func TestGetFile_NotFound(t *testing.T) { s := newFileHandlerSetup() s.mock.getFileMetadataFn = func(ctx context.Context, fileID int64) (*model.File, *model.FileBlob, error) { return nil, nil, fmt.Errorf("file not found: 999") } w := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodGet, "/api/v1/files/999", nil) s.router.ServeHTTP(w, req) if w.Code != http.StatusInternalServerError { t.Fatalf("expected 500, got %d: %s", w.Code, w.Body.String()) } } func TestGetFile_InvalidID(t *testing.T) { s := newFileHandlerSetup() w := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodGet, "/api/v1/files/abc", nil) s.router.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { t.Fatalf("expected 400, got %d", w.Code) } } // ---- DownloadFile Tests ---- func TestDownloadFile_Full(t *testing.T) { s := newFileHandlerSetup() content := "hello world file content" now := time.Now() s.mock.downloadFileFn = func(ctx context.Context, fileID int64, rangeHeader string) (io.ReadCloser, *model.File, *model.FileBlob, int64, int64, error) { reader := io.NopCloser(strings.NewReader(content)) return reader, &model.File{ID: 1, Name: "test.txt", CreatedAt: now}, &model.FileBlob{FileSize: int64(len(content)), MimeType: "text/plain"}, 0, int64(len(content)) - 1, nil } w := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodGet, "/api/v1/files/1/download", nil) s.router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) } // Check streaming headers if got := w.Header().Get("Content-Disposition"); !strings.Contains(got, "test.txt") { t.Fatalf("expected Content-Disposition to contain 'test.txt', got %s", got) } if got := w.Header().Get("Content-Type"); got != "text/plain" { t.Fatalf("expected Content-Type=text/plain, got %s", got) } if got := w.Header().Get("Accept-Ranges"); got != "bytes" { t.Fatalf("expected Accept-Ranges=bytes, got %s", got) } if w.Body.String() != content { t.Fatalf("expected body %q, got %q", content, w.Body.String()) } } func TestDownloadFile_WithRange(t *testing.T) { s := newFileHandlerSetup() content := "hello world" now := time.Now() s.mock.downloadFileFn = func(ctx context.Context, fileID int64, rangeHeader string) (io.ReadCloser, *model.File, *model.FileBlob, int64, int64, error) { reader := io.NopCloser(strings.NewReader(content[0:5])) return reader, &model.File{ID: 1, Name: "test.txt", CreatedAt: now}, &model.FileBlob{FileSize: int64(len(content)), MimeType: "text/plain"}, 0, 4, nil } w := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodGet, "/api/v1/files/1/download", nil) req.Header.Set("Range", "bytes=0-4") s.router.ServeHTTP(w, req) if w.Code != http.StatusPartialContent { t.Fatalf("expected 206, got %d: %s", w.Code, w.Body.String()) } // Check Content-Range header if got := w.Header().Get("Content-Range"); got != "bytes 0-4/11" { t.Fatalf("expected Content-Range 'bytes 0-4/11', got %s", got) } if got := w.Header().Get("Content-Type"); got != "text/plain" { t.Fatalf("expected Content-Type=text/plain, got %s", got) } if got := w.Header().Get("Content-Length"); got != "5" { t.Fatalf("expected Content-Length=5, got %s", got) } } func TestDownloadFile_InvalidID(t *testing.T) { s := newFileHandlerSetup() w := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodGet, "/api/v1/files/abc/download", nil) s.router.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { t.Fatalf("expected 400, got %d", w.Code) } } func TestDownloadFile_ServiceError(t *testing.T) { s := newFileHandlerSetup() s.mock.downloadFileFn = func(ctx context.Context, fileID int64, rangeHeader string) (io.ReadCloser, *model.File, *model.FileBlob, int64, int64, error) { return nil, nil, nil, 0, 0, fmt.Errorf("file not found: 999") } w := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodGet, "/api/v1/files/999/download", nil) s.router.ServeHTTP(w, req) if w.Code != http.StatusInternalServerError { t.Fatalf("expected 500, got %d: %s", w.Code, w.Body.String()) } } // ---- DeleteFile Tests ---- func TestDeleteFile_Success(t *testing.T) { s := newFileHandlerSetup() s.mock.deleteFileFn = func(ctx context.Context, fileID int64) error { return nil } w := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodDelete, "/api/v1/files/1", nil) s.router.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["message"] != "file deleted" { t.Fatalf("expected message 'file deleted', got %v", data["message"]) } } func TestDeleteFile_InvalidID(t *testing.T) { s := newFileHandlerSetup() w := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodDelete, "/api/v1/files/abc", nil) s.router.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { t.Fatalf("expected 400, got %d", w.Code) } } func TestDeleteFile_ServiceError(t *testing.T) { s := newFileHandlerSetup() s.mock.deleteFileFn = func(ctx context.Context, fileID int64) error { return fmt.Errorf("file not found: 999") } w := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodDelete, "/api/v1/files/999", nil) s.router.ServeHTTP(w, req) if w.Code != http.StatusInternalServerError { t.Fatalf("expected 500, got %d: %s", w.Code, w.Body.String()) } }