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>
370 lines
11 KiB
Go
370 lines
11 KiB
Go
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())
|
|
}
|
|
}
|