Add file management API with local storage backend
- Implement FileHandler with CRUD operations for files/directories - Add FileService with business logic and SHA-256 hashing - Create LocalStorage backend for filesystem persistence - Add database repository with pagination and name uniqueness constraints - Configure max upload size in storage settings - Include comprehensive tests for all layers
This commit is contained in:
@@ -14,12 +14,6 @@ import (
|
||||
"github.com/dhao2001/mygo/internal/service"
|
||||
)
|
||||
|
||||
func setupAccountHandler(t *testing.T) (*AccountHandler, []byte) {
|
||||
t.Helper()
|
||||
svc, secret := setupTestAuthService(t)
|
||||
return NewAccountHandler(svc), secret
|
||||
}
|
||||
|
||||
func setupAccountRouter(t *testing.T) (*gin.Engine, []byte) {
|
||||
t.Helper()
|
||||
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/dhao2001/mygo/internal/api"
|
||||
"github.com/dhao2001/mygo/internal/middleware"
|
||||
"github.com/dhao2001/mygo/internal/model"
|
||||
"github.com/dhao2001/mygo/internal/service"
|
||||
)
|
||||
|
||||
// FileHandler handles file management endpoints.
|
||||
type FileHandler struct {
|
||||
fileService *service.FileService
|
||||
}
|
||||
|
||||
// NewFileHandler creates a FileHandler.
|
||||
func NewFileHandler(fileService *service.FileService) *FileHandler {
|
||||
return &FileHandler{fileService: fileService}
|
||||
}
|
||||
|
||||
type createDirRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
ParentID *string `json:"parent_id"`
|
||||
}
|
||||
|
||||
type updateFileRequest struct {
|
||||
Name string `json:"name"`
|
||||
ParentID *string `json:"parent_id"`
|
||||
}
|
||||
|
||||
// Upload handles POST /api/v1/files.
|
||||
// If the content type is multipart/form-data, it uploads a file.
|
||||
// If the content type is application/json, it creates a directory.
|
||||
func (h *FileHandler) Upload(c *gin.Context) {
|
||||
userID := middleware.GetUserID(c)
|
||||
contentType := c.GetHeader("Content-Type")
|
||||
|
||||
// Directory creation via JSON.
|
||||
if len(contentType) >= 16 && contentType[:16] == "application/json" {
|
||||
var req createDirRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
api.Error(c, http.StatusBadRequest, "invalid request: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
dir, err := h.fileService.CreateDir(c.Request.Context(), userID, req.ParentID, req.Name)
|
||||
if err != nil {
|
||||
api.Error(c, mapError(err), err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, dir)
|
||||
return
|
||||
}
|
||||
|
||||
// File upload via multipart.
|
||||
if err := c.Request.ParseMultipartForm(32 << 20); err != nil {
|
||||
api.Error(c, http.StatusBadRequest, "invalid multipart form: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
parentIDStr := c.PostForm("parent_id")
|
||||
var parentID *string
|
||||
if parentIDStr != "" {
|
||||
parentID = &parentIDStr
|
||||
}
|
||||
|
||||
header, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
api.Error(c, http.StatusBadRequest, "missing file field: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
file, err := header.Open()
|
||||
if err != nil {
|
||||
api.Error(c, http.StatusInternalServerError, "open uploaded file: "+err.Error())
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
info, err := h.fileService.Upload(c.Request.Context(), userID, parentID, header.Filename, file, header.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
api.Error(c, mapError(err), err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, info)
|
||||
}
|
||||
|
||||
// List handles GET /api/v1/files.
|
||||
func (h *FileHandler) List(c *gin.Context) {
|
||||
userID := middleware.GetUserID(c)
|
||||
|
||||
parentIDStr := c.Query("parent_id")
|
||||
var parentID *string
|
||||
if parentIDStr != "" {
|
||||
parentID = &parentIDStr
|
||||
}
|
||||
|
||||
offset, err := strconv.Atoi(c.DefaultQuery("offset", "0"))
|
||||
if err != nil || offset < 0 {
|
||||
api.Error(c, http.StatusBadRequest, "invalid offset")
|
||||
return
|
||||
}
|
||||
|
||||
limit, err := strconv.Atoi(c.DefaultQuery("limit", "50"))
|
||||
if err != nil || limit < 1 {
|
||||
api.Error(c, http.StatusBadRequest, "invalid limit")
|
||||
return
|
||||
}
|
||||
if limit > 200 {
|
||||
limit = 200
|
||||
}
|
||||
|
||||
list, err := h.fileService.List(c.Request.Context(), userID, parentID, offset, limit)
|
||||
if err != nil {
|
||||
api.Error(c, mapError(err), err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, list)
|
||||
}
|
||||
|
||||
// Get handles GET /api/v1/files/:id.
|
||||
func (h *FileHandler) Get(c *gin.Context) {
|
||||
userID := middleware.GetUserID(c)
|
||||
fileID := c.Param("id")
|
||||
|
||||
info, err := h.fileService.Get(c.Request.Context(), userID, fileID)
|
||||
if err != nil {
|
||||
api.Error(c, mapError(err), err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, info)
|
||||
}
|
||||
|
||||
// Download handles GET /api/v1/files/:id/content.
|
||||
func (h *FileHandler) Download(c *gin.Context) {
|
||||
userID := middleware.GetUserID(c)
|
||||
fileID := c.Param("id")
|
||||
|
||||
reader, info, err := h.fileService.Download(c.Request.Context(), userID, fileID)
|
||||
if err != nil {
|
||||
api.Error(c, mapError(err), err.Error())
|
||||
return
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
mimeType := info.MimeType
|
||||
if mimeType == "" {
|
||||
mimeType = "application/octet-stream"
|
||||
}
|
||||
|
||||
c.Header("Content-Type", mimeType)
|
||||
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", url.PathEscape(info.Name)))
|
||||
c.Header("Content-Length", strconv.FormatInt(info.Size, 10))
|
||||
|
||||
c.Status(http.StatusOK)
|
||||
io.Copy(c.Writer, reader)
|
||||
}
|
||||
|
||||
// Update handles PUT /api/v1/files/:id.
|
||||
func (h *FileHandler) Update(c *gin.Context) {
|
||||
userID := middleware.GetUserID(c)
|
||||
fileID := c.Param("id")
|
||||
|
||||
var req updateFileRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
api.Error(c, http.StatusBadRequest, "invalid request: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Validate at least one field is provided.
|
||||
if req.Name == "" && req.ParentID == nil {
|
||||
api.Error(c, http.StatusBadRequest, "at least one of name or parent_id must be provided")
|
||||
return
|
||||
}
|
||||
|
||||
info, err := h.fileService.Update(c.Request.Context(), userID, fileID, req.Name, req.ParentID)
|
||||
if err != nil {
|
||||
api.Error(c, mapError(err), err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, info)
|
||||
}
|
||||
|
||||
// Delete handles DELETE /api/v1/files/:id.
|
||||
func (h *FileHandler) Delete(c *gin.Context) {
|
||||
userID := middleware.GetUserID(c)
|
||||
fileID := c.Param("id")
|
||||
|
||||
if err := h.fileService.Delete(c.Request.Context(), userID, fileID); err != nil {
|
||||
api.Error(c, mapError(err), err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// mapError maps sentinel errors to HTTP status codes.
|
||||
func mapError(err error) int {
|
||||
switch {
|
||||
case errors.Is(err, model.ErrNotFound):
|
||||
return http.StatusNotFound
|
||||
case errors.Is(err, model.ErrForbidden):
|
||||
return http.StatusForbidden
|
||||
case errors.Is(err, model.ErrDuplicate):
|
||||
return http.StatusConflict
|
||||
default:
|
||||
return http.StatusInternalServerError
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,439 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/dhao2001/mygo/internal/model"
|
||||
"github.com/dhao2001/mygo/internal/repository"
|
||||
"github.com/dhao2001/mygo/internal/service"
|
||||
"github.com/dhao2001/mygo/internal/storage"
|
||||
)
|
||||
|
||||
// inMemStore implements storage.StorageBackend with an in-memory map.
|
||||
type inMemStore struct {
|
||||
files map[string][]byte
|
||||
}
|
||||
|
||||
func newInMemStore() *inMemStore {
|
||||
return &inMemStore{files: make(map[string][]byte)}
|
||||
}
|
||||
|
||||
func (s *inMemStore) Save(_ context.Context, path string, reader io.Reader) (int64, error) {
|
||||
data, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
s.files[path] = data
|
||||
return int64(len(data)), nil
|
||||
}
|
||||
|
||||
func (s *inMemStore) Open(_ context.Context, path string) (io.ReadCloser, error) {
|
||||
data, ok := s.files[path]
|
||||
if !ok {
|
||||
return nil, &fsNotFoundError{path}
|
||||
}
|
||||
return io.NopCloser(bytes.NewReader(data)), nil
|
||||
}
|
||||
|
||||
func (s *inMemStore) Delete(_ context.Context, path string) error {
|
||||
delete(s.files, path)
|
||||
return nil
|
||||
}
|
||||
|
||||
type fsNotFoundError struct{ path string }
|
||||
|
||||
func (e *fsNotFoundError) Error() string { return "file not found on disk: " + e.path }
|
||||
|
||||
var _ storage.StorageBackend = (*inMemStore)(nil)
|
||||
|
||||
func setupFileHandler(t *testing.T) *FileHandler {
|
||||
t.Helper()
|
||||
|
||||
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatalf("open db: %v", err)
|
||||
}
|
||||
if err := db.AutoMigrate(&model.File{}); err != nil {
|
||||
t.Fatalf("migrate: %v", err)
|
||||
}
|
||||
|
||||
fileRepo := repository.NewFileRepository(db)
|
||||
store := newInMemStore()
|
||||
fileService := service.NewFileService(fileRepo, store, 0)
|
||||
|
||||
return NewFileHandler(fileService)
|
||||
}
|
||||
|
||||
func setupFileRouter(t *testing.T) *gin.Engine {
|
||||
t.Helper()
|
||||
|
||||
handler := setupFileHandler(t)
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
r := gin.New()
|
||||
|
||||
// Middleware that injects user_id from header (simulates AuthRequired).
|
||||
r.Use(func(c *gin.Context) {
|
||||
userID := c.GetHeader("X-User-ID")
|
||||
if userID != "" {
|
||||
c.Set("user_id", userID)
|
||||
}
|
||||
c.Next()
|
||||
})
|
||||
|
||||
files := r.Group("/api/v1/files")
|
||||
{
|
||||
files.GET("", handler.List)
|
||||
files.POST("", handler.Upload)
|
||||
files.GET("/:id", handler.Get)
|
||||
files.GET("/:id/content", handler.Download)
|
||||
files.PUT("/:id", handler.Update)
|
||||
files.DELETE("/:id", handler.Delete)
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func TestFileHandler_Upload(t *testing.T) {
|
||||
r := setupFileRouter(t)
|
||||
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
part, _ := writer.CreateFormFile("file", "test.txt")
|
||||
part.Write([]byte("hello upload"))
|
||||
writer.Close()
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/files", body)
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
req.Header.Set("X-User-ID", "user1")
|
||||
rec := httptest.NewRecorder()
|
||||
r.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusCreated {
|
||||
t.Errorf("status = %d, want %d; body = %s", rec.Code, http.StatusCreated, rec.Body.String())
|
||||
}
|
||||
|
||||
var info service.FileInfo
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &info); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
if info.Name != "test.txt" {
|
||||
t.Errorf("Name = %q, want %q", info.Name, "test.txt")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileHandler_UploadNoFile(t *testing.T) {
|
||||
r := setupFileRouter(t)
|
||||
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
writer.Close()
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/files", body)
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
req.Header.Set("X-User-ID", "user1")
|
||||
rec := httptest.NewRecorder()
|
||||
r.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusBadRequest {
|
||||
t.Errorf("status = %d, want %d", rec.Code, http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileHandler_CreateDir(t *testing.T) {
|
||||
r := setupFileRouter(t)
|
||||
|
||||
body, _ := json.Marshal(gin.H{"name": "mydir"})
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/files", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-User-ID", "user1")
|
||||
rec := httptest.NewRecorder()
|
||||
r.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusCreated {
|
||||
t.Errorf("status = %d, want %d; body = %s", rec.Code, http.StatusCreated, rec.Body.String())
|
||||
}
|
||||
|
||||
var info service.FileInfo
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &info); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
if !info.IsDir {
|
||||
t.Error("IsDir should be true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileHandler_List(t *testing.T) {
|
||||
r := setupFileRouter(t)
|
||||
|
||||
// Upload a file first.
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
part, _ := writer.CreateFormFile("file", "file1.txt")
|
||||
part.Write([]byte("content1"))
|
||||
writer.Close()
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/files", body)
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
req.Header.Set("X-User-ID", "user1")
|
||||
rec := httptest.NewRecorder()
|
||||
r.ServeHTTP(rec, req)
|
||||
|
||||
// List files.
|
||||
req = httptest.NewRequest(http.MethodGet, "/api/v1/files", nil)
|
||||
req.Header.Set("X-User-ID", "user1")
|
||||
rec = httptest.NewRecorder()
|
||||
r.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Errorf("status = %d, want %d; body = %s", rec.Code, http.StatusOK, rec.Body.String())
|
||||
}
|
||||
|
||||
var list service.FileList
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &list); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
if list.Total != 1 {
|
||||
t.Errorf("Total = %d, want 1", list.Total)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileHandler_Get(t *testing.T) {
|
||||
r := setupFileRouter(t)
|
||||
|
||||
// Upload a file first.
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
part, _ := writer.CreateFormFile("file", "info.txt")
|
||||
part.Write([]byte("file info content"))
|
||||
writer.Close()
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/files", body)
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
req.Header.Set("X-User-ID", "user1")
|
||||
rec := httptest.NewRecorder()
|
||||
r.ServeHTTP(rec, req)
|
||||
|
||||
var uploaded service.FileInfo
|
||||
json.Unmarshal(rec.Body.Bytes(), &uploaded)
|
||||
|
||||
// Get metadata.
|
||||
req = httptest.NewRequest(http.MethodGet, "/api/v1/files/"+uploaded.ID, nil)
|
||||
req.Header.Set("X-User-ID", "user1")
|
||||
rec = httptest.NewRecorder()
|
||||
r.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Errorf("status = %d, want %d", rec.Code, http.StatusOK)
|
||||
}
|
||||
|
||||
var info service.FileInfo
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &info); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
if info.ID != uploaded.ID {
|
||||
t.Errorf("ID = %q, want %q", info.ID, uploaded.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileHandler_GetNotFound(t *testing.T) {
|
||||
r := setupFileRouter(t)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/files/nonexistent", nil)
|
||||
req.Header.Set("X-User-ID", "user1")
|
||||
rec := httptest.NewRecorder()
|
||||
r.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusNotFound {
|
||||
t.Errorf("status = %d, want %d", rec.Code, http.StatusNotFound)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileHandler_Download(t *testing.T) {
|
||||
r := setupFileRouter(t)
|
||||
|
||||
// Upload a file.
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
part, _ := writer.CreateFormFile("file", "download.txt")
|
||||
content := []byte("download me please")
|
||||
part.Write(content)
|
||||
writer.Close()
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/files", body)
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
req.Header.Set("X-User-ID", "user1")
|
||||
rec := httptest.NewRecorder()
|
||||
r.ServeHTTP(rec, req)
|
||||
|
||||
var uploaded service.FileInfo
|
||||
json.Unmarshal(rec.Body.Bytes(), &uploaded)
|
||||
|
||||
// Download.
|
||||
req = httptest.NewRequest(http.MethodGet, "/api/v1/files/"+uploaded.ID+"/content", nil)
|
||||
req.Header.Set("X-User-ID", "user1")
|
||||
rec = httptest.NewRecorder()
|
||||
r.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Errorf("status = %d, want %d", rec.Code, http.StatusOK)
|
||||
}
|
||||
if !bytes.Equal(rec.Body.Bytes(), content) {
|
||||
t.Errorf("content = %q, want %q", rec.Body.String(), content)
|
||||
}
|
||||
if rec.Header().Get("Content-Disposition") == "" {
|
||||
t.Error("Content-Disposition header should be set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileHandler_Update(t *testing.T) {
|
||||
r := setupFileRouter(t)
|
||||
|
||||
// Upload a file.
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
part, _ := writer.CreateFormFile("file", "oldname.txt")
|
||||
part.Write([]byte("content"))
|
||||
writer.Close()
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/files", body)
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
req.Header.Set("X-User-ID", "user1")
|
||||
rec := httptest.NewRecorder()
|
||||
r.ServeHTTP(rec, req)
|
||||
|
||||
var uploaded service.FileInfo
|
||||
json.Unmarshal(rec.Body.Bytes(), &uploaded)
|
||||
|
||||
// Rename.
|
||||
updateBody, _ := json.Marshal(gin.H{"name": "newname.txt"})
|
||||
req = httptest.NewRequest(http.MethodPut, "/api/v1/files/"+uploaded.ID, bytes.NewReader(updateBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-User-ID", "user1")
|
||||
rec = httptest.NewRecorder()
|
||||
r.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Errorf("status = %d, want %d; body = %s", rec.Code, http.StatusOK, rec.Body.String())
|
||||
}
|
||||
|
||||
var updated service.FileInfo
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &updated); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
if updated.Name != "newname.txt" {
|
||||
t.Errorf("Name = %q, want %q", updated.Name, "newname.txt")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileHandler_UpdateNoFields(t *testing.T) {
|
||||
r := setupFileRouter(t)
|
||||
|
||||
// Upload a file.
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
part, _ := writer.CreateFormFile("file", "file.txt")
|
||||
part.Write([]byte("content"))
|
||||
writer.Close()
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/files", body)
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
req.Header.Set("X-User-ID", "user1")
|
||||
rec := httptest.NewRecorder()
|
||||
r.ServeHTTP(rec, req)
|
||||
|
||||
var uploaded service.FileInfo
|
||||
json.Unmarshal(rec.Body.Bytes(), &uploaded)
|
||||
|
||||
updateBody, _ := json.Marshal(gin.H{})
|
||||
req = httptest.NewRequest(http.MethodPut, "/api/v1/files/"+uploaded.ID, bytes.NewReader(updateBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-User-ID", "user1")
|
||||
rec = httptest.NewRecorder()
|
||||
r.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusBadRequest {
|
||||
t.Errorf("status = %d, want %d", rec.Code, http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileHandler_Delete(t *testing.T) {
|
||||
r := setupFileRouter(t)
|
||||
|
||||
// Upload a file.
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
part, _ := writer.CreateFormFile("file", "delete-me.txt")
|
||||
part.Write([]byte("content"))
|
||||
writer.Close()
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/files", body)
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
req.Header.Set("X-User-ID", "user1")
|
||||
rec := httptest.NewRecorder()
|
||||
r.ServeHTTP(rec, req)
|
||||
|
||||
var uploaded service.FileInfo
|
||||
json.Unmarshal(rec.Body.Bytes(), &uploaded)
|
||||
|
||||
// Delete.
|
||||
req = httptest.NewRequest(http.MethodDelete, "/api/v1/files/"+uploaded.ID, nil)
|
||||
req.Header.Set("X-User-ID", "user1")
|
||||
rec = httptest.NewRecorder()
|
||||
r.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusNoContent {
|
||||
t.Errorf("status = %d, want %d; body = %s", rec.Code, http.StatusNoContent, rec.Body.String())
|
||||
}
|
||||
|
||||
// Verify gone.
|
||||
req = httptest.NewRequest(http.MethodGet, "/api/v1/files/"+uploaded.ID, nil)
|
||||
req.Header.Set("X-User-ID", "user1")
|
||||
rec = httptest.NewRecorder()
|
||||
r.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusNotFound {
|
||||
t.Errorf("status after delete = %d, want %d", rec.Code, http.StatusNotFound)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileHandler_ForbiddenAccess(t *testing.T) {
|
||||
r := setupFileRouter(t)
|
||||
|
||||
// Upload as user1.
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
part, _ := writer.CreateFormFile("file", "private.txt")
|
||||
part.Write([]byte("secret"))
|
||||
writer.Close()
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/files", body)
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
req.Header.Set("X-User-ID", "user1")
|
||||
rec := httptest.NewRecorder()
|
||||
r.ServeHTTP(rec, req)
|
||||
|
||||
var uploaded service.FileInfo
|
||||
json.Unmarshal(rec.Body.Bytes(), &uploaded)
|
||||
|
||||
// Try to access as user2.
|
||||
req = httptest.NewRequest(http.MethodGet, "/api/v1/files/"+uploaded.ID, nil)
|
||||
req.Header.Set("X-User-ID", "user2")
|
||||
rec = httptest.NewRecorder()
|
||||
r.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusForbidden {
|
||||
t.Errorf("status = %d, want %d", rec.Code, http.StatusForbidden)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user