From 1803eb5b08d254d57620f6e41285041d58568feb Mon Sep 17 00:00:00 2001 From: AmirAgassi <33383085+AmirAgassi@users.noreply.github.com> Date: Wed, 18 Dec 2024 02:14:32 -0500 Subject: [PATCH 1/4] add file check middleware --- backend/internal/middleware/upload.go | 80 ++++++++++++++ backend/internal/middleware/upload_test.go | 115 +++++++++++++++++++++ 2 files changed, 195 insertions(+) create mode 100644 backend/internal/middleware/upload.go create mode 100644 backend/internal/middleware/upload_test.go diff --git a/backend/internal/middleware/upload.go b/backend/internal/middleware/upload.go new file mode 100644 index 00000000..25b22434 --- /dev/null +++ b/backend/internal/middleware/upload.go @@ -0,0 +1,80 @@ +package middleware + +import ( + "fmt" + "net/http" + + "github.com/labstack/echo/v4" +) + +/* +FileSizeConfig holds configuration for the file size check middleware. +MinSize and MaxSize are in bytes. + +Example: + 1MB = 1 * 1024 * 1024 bytes + 10MB = 10 * 1024 * 1024 bytes +*/ +type FileSizeConfig struct { + MinSize int64 + MaxSize int64 +} + +/* +FileSizeCheck middleware ensures uploaded files are within specified size limits. +It checks the Content-Length header and returns 413 if file is too large +or 400 if file is too small. + +Usage: + e.POST("/upload", handler, middleware.FileSizeCheck(middleware.FileSizeConfig{ + MinSize: 1024, // 1KB minimum + MaxSize: 10485760, // 10MB maximum + })) +*/ +func FileSizeCheck(config FileSizeConfig) echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + // only check on requests that might have file uploads + if c.Request().Method != http.MethodPost && c.Request().Method != http.MethodPut { + return next(c) + } + + // first check content-length as early rejection + contentLength := c.Request().ContentLength + if contentLength == -1 { + return echo.NewHTTPError(http.StatusBadRequest, "content length required") + } + + if contentLength > config.MaxSize { + return echo.NewHTTPError(http.StatusRequestEntityTooLarge, + fmt.Sprintf("file size %d exceeds maximum allowed size of %d", contentLength, config.MaxSize)) + } + + // parse multipart form with max size limit to prevent memory exhaustion + if err := c.Request().ParseMultipartForm(config.MaxSize); err != nil { + return echo.NewHTTPError(http.StatusRequestEntityTooLarge, "file too large") + } + + // check actual file sizes if the content-length check passed + // (i don't think it would ever happen, but clients can fake a content-length header) + form := c.Request().MultipartForm + if form != nil && form.File != nil { + for _, files := range form.File { + for _, file := range files { + size := file.Size + if size > config.MaxSize { + return echo.NewHTTPError(http.StatusRequestEntityTooLarge, + fmt.Sprintf("file %s size %d exceeds maximum allowed size of %d", file.Filename, size, config.MaxSize)) + } + if size < config.MinSize { + return echo.NewHTTPError(http.StatusBadRequest, + fmt.Sprintf("file %s size %d below minimum required size of %d", file.Filename, size, config.MinSize)) + } + } + } + } + + return next(c) + } + } +} \ No newline at end of file diff --git a/backend/internal/middleware/upload_test.go b/backend/internal/middleware/upload_test.go new file mode 100644 index 00000000..c129de25 --- /dev/null +++ b/backend/internal/middleware/upload_test.go @@ -0,0 +1,115 @@ +package middleware + +import ( + "bytes" + "mime/multipart" + "net/http" + "net/http/httptest" + "testing" + + "github.com/labstack/echo/v4" + "github.com/stretchr/testify/assert" +) + +func TestFileSizeCheck(t *testing.T) { + e := echo.New() + + handler := func(c echo.Context) error { + return c.String(http.StatusOK, "success") + } + + // helper to create multipart request with a file + createMultipartRequest := func(filename string, content []byte) (*http.Request, *bytes.Buffer, error) { + body := new(bytes.Buffer) + writer := multipart.NewWriter(body) + part, err := writer.CreateFormFile("file", filename) + if err != nil { + return nil, nil, err + } + part.Write(content) + writer.Close() + + req := httptest.NewRequest(http.MethodPost, "/", body) + req.Header.Set("Content-Type", writer.FormDataContentType()) + req.ContentLength = int64(body.Len()) + return req, body, nil + } + + tests := []struct { + name string + config FileSizeConfig + fileSize int + expectedStatus int + }{ + { + name: "valid file size", + config: FileSizeConfig{ + MinSize: 5, + MaxSize: 1024, // increased to account for form overhead + }, + fileSize: 50, + expectedStatus: http.StatusOK, + }, + { + name: "file too large", + config: FileSizeConfig{ + MinSize: 5, + MaxSize: 100, + }, + fileSize: 150, + expectedStatus: http.StatusRequestEntityTooLarge, + }, + { + name: "file too small", + config: FileSizeConfig{ + MinSize: 50, + MaxSize: 1024, + }, + fileSize: 10, + expectedStatus: http.StatusBadRequest, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // create a file with specified size + content := make([]byte, tt.fileSize) + req, body, err := createMultipartRequest("test.txt", content) + assert.NoError(t, err) + + // log actual size for debugging + t.Logf("Total request size: %d, File content size: %d", body.Len(), tt.fileSize) + + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + + // wrap handler with middleware + h := FileSizeCheck(tt.config)(handler) + err = h(c) + + if tt.expectedStatus != http.StatusOK { + he, ok := err.(*echo.HTTPError) + assert.True(t, ok) + assert.Equal(t, tt.expectedStatus, he.Code) + } else { + assert.NoError(t, err) + } + }) + } + + // Test GET request (should skip check) + t.Run("skip check for GET request", func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/", nil) + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + + h := FileSizeCheck(FileSizeConfig{ + MinSize: 5, + MaxSize: 100, + })(handler) + + err := h(c) + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, rec.Code) + }) +} \ No newline at end of file From 9da8f1aa50e41eaa8bc4e4fccbdeba9804be527a Mon Sep 17 00:00:00 2001 From: AmirAgassi <33383085+AmirAgassi@users.noreply.github.com> Date: Wed, 18 Dec 2024 16:00:42 -0500 Subject: [PATCH 2/4] Update filenames --- backend/internal/middleware/{upload.go => file_size_check.go} | 0 .../middleware/{upload_test.go => file_size_check_test.go} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename backend/internal/middleware/{upload.go => file_size_check.go} (100%) rename backend/internal/middleware/{upload_test.go => file_size_check_test.go} (100%) diff --git a/backend/internal/middleware/upload.go b/backend/internal/middleware/file_size_check.go similarity index 100% rename from backend/internal/middleware/upload.go rename to backend/internal/middleware/file_size_check.go diff --git a/backend/internal/middleware/upload_test.go b/backend/internal/middleware/file_size_check_test.go similarity index 100% rename from backend/internal/middleware/upload_test.go rename to backend/internal/middleware/file_size_check_test.go From 78b3a529ff21c4a731842a52f1a8782b78665913 Mon Sep 17 00:00:00 2001 From: AmirAgassi <33383085+AmirAgassi@users.noreply.github.com> Date: Wed, 18 Dec 2024 16:04:29 -0500 Subject: [PATCH 3/4] add MIME type validation to file upload middleware --- backend/internal/middleware/file_check.go | 124 ++++++++++++++++++ ..._size_check_test.go => file_check_test.go} | 64 +++++---- .../internal/middleware/file_size_check.go | 80 ----------- 3 files changed, 162 insertions(+), 106 deletions(-) create mode 100644 backend/internal/middleware/file_check.go rename backend/internal/middleware/{file_size_check_test.go => file_check_test.go} (62%) delete mode 100644 backend/internal/middleware/file_size_check.go diff --git a/backend/internal/middleware/file_check.go b/backend/internal/middleware/file_check.go new file mode 100644 index 00000000..c245da6b --- /dev/null +++ b/backend/internal/middleware/file_check.go @@ -0,0 +1,124 @@ +package middleware + +import ( + "fmt" + "mime/multipart" + "net/http" + + "github.com/gabriel-vasile/mimetype" + "github.com/labstack/echo/v4" +) + +/* +FileConfig holds configuration for the file validation middleware. +MinSize and MaxSize are in bytes. + +Example: + 1MB = 1 * 1024 * 1024 bytes + 10MB = 10 * 1024 * 1024 bytes +*/ +type FileConfig struct { + MinSize int64 + MaxSize int64 + AllowedTypes []string // e.g. ["image/jpeg", "image/png", "application/pdf"] +} + +/* +FileCheck middleware ensures uploaded files meet specified criteria: +- Size limits (via Content-Length header and actual file size) +- MIME type validation + +Usage: + e.POST("/upload", handler, middleware.FileCheck(middleware.FileConfig{ + MinSize: 1024, // 1KB minimum + MaxSize: 10485760, // 10MB maximum + AllowedTypes: []string{ + "image/jpeg", + "image/png", + "application/pdf", + }, + })) +*/ +func FileCheck(config FileConfig) echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + // only check on requests that might have file uploads + if c.Request().Method != http.MethodPost && c.Request().Method != http.MethodPut { + return next(c) + } + + // first check content-length as early rejection + contentLength := c.Request().ContentLength + if contentLength == -1 { + return echo.NewHTTPError(http.StatusBadRequest, "content length required") + } + + if contentLength > config.MaxSize { + return echo.NewHTTPError(http.StatusRequestEntityTooLarge, + fmt.Sprintf("file size %d exceeds maximum allowed size of %d", contentLength, config.MaxSize)) + } + + // parse multipart form with max size limit to prevent memory exhaustion + if err := c.Request().ParseMultipartForm(config.MaxSize); err != nil { + return echo.NewHTTPError(http.StatusRequestEntityTooLarge, "file too large") + } + + // check actual file sizes and MIME types + form := c.Request().MultipartForm + if form != nil && form.File != nil { + for _, files := range form.File { + for _, file := range files { + if err := validateFile(file, config); err != nil { + return err + } + } + } + } + + return next(c) + } + } +} + +// validateFile checks both size and MIME type of a single file +func validateFile(file *multipart.FileHeader, config FileConfig) error { + // Check file size + size := file.Size + if size > config.MaxSize { + return echo.NewHTTPError(http.StatusRequestEntityTooLarge, + fmt.Sprintf("file %s size %d exceeds maximum allowed size of %d", file.Filename, size, config.MaxSize)) + } + if size < config.MinSize { + return echo.NewHTTPError(http.StatusBadRequest, + fmt.Sprintf("file %s size %d below minimum required size of %d", file.Filename, size, config.MinSize)) + } + + // Check MIME type if restrictions are specified + if len(config.AllowedTypes) > 0 { + f, err := file.Open() + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "could not read file") + } + defer f.Close() + + mime, err := mimetype.DetectReader(f) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "could not detect file type") + } + + isAllowed := false + for _, allowed := range config.AllowedTypes { + if mime.Is(allowed) { + isAllowed = true + break + } + } + + if !isAllowed { + return echo.NewHTTPError(http.StatusBadRequest, + fmt.Sprintf("file type %s not allowed. Allowed types: %v", mime.String(), config.AllowedTypes)) + } + } + + return nil +} \ No newline at end of file diff --git a/backend/internal/middleware/file_size_check_test.go b/backend/internal/middleware/file_check_test.go similarity index 62% rename from backend/internal/middleware/file_size_check_test.go rename to backend/internal/middleware/file_check_test.go index c129de25..79be75c6 100644 --- a/backend/internal/middleware/file_size_check_test.go +++ b/backend/internal/middleware/file_check_test.go @@ -11,7 +11,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestFileSizeCheck(t *testing.T) { +func TestFileCheck(t *testing.T) { e := echo.New() handler := func(c echo.Context) error { @@ -37,54 +37,66 @@ func TestFileSizeCheck(t *testing.T) { tests := []struct { name string - config FileSizeConfig - fileSize int + config FileConfig + filename string + content []byte expectedStatus int }{ { - name: "valid file size", - config: FileSizeConfig{ - MinSize: 5, - MaxSize: 1024, // increased to account for form overhead + name: "valid file", + config: FileConfig{ + MinSize: 5, + MaxSize: 1024, + AllowedTypes: []string{"text/plain"}, }, - fileSize: 50, + filename: "test.txt", + content: []byte("Hello, World!"), expectedStatus: http.StatusOK, }, { name: "file too large", - config: FileSizeConfig{ - MinSize: 5, - MaxSize: 100, + config: FileConfig{ + MinSize: 5, + MaxSize: 100, + AllowedTypes: []string{"text/plain"}, }, - fileSize: 150, + filename: "large.txt", + content: bytes.Repeat([]byte("a"), 150), expectedStatus: http.StatusRequestEntityTooLarge, }, { name: "file too small", - config: FileSizeConfig{ - MinSize: 50, - MaxSize: 1024, + config: FileConfig{ + MinSize: 50, + MaxSize: 1024, + AllowedTypes: []string{"text/plain"}, }, - fileSize: 10, + filename: "small.txt", + content: []byte("tiny"), + expectedStatus: http.StatusBadRequest, + }, + { + name: "invalid mime type", + config: FileConfig{ + MinSize: 5, + MaxSize: 1024, + AllowedTypes: []string{"image/jpeg", "image/png"}, + }, + filename: "test.txt", + content: []byte("Hello, World!"), expectedStatus: http.StatusBadRequest, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // create a file with specified size - content := make([]byte, tt.fileSize) - req, body, err := createMultipartRequest("test.txt", content) + req, _, err := createMultipartRequest(tt.filename, tt.content) assert.NoError(t, err) - // log actual size for debugging - t.Logf("Total request size: %d, File content size: %d", body.Len(), tt.fileSize) - rec := httptest.NewRecorder() c := e.NewContext(req, rec) - // wrap handler with middleware - h := FileSizeCheck(tt.config)(handler) + h := FileCheck(tt.config)(handler) err = h(c) if tt.expectedStatus != http.StatusOK { @@ -103,7 +115,7 @@ func TestFileSizeCheck(t *testing.T) { rec := httptest.NewRecorder() c := e.NewContext(req, rec) - h := FileSizeCheck(FileSizeConfig{ + h := FileCheck(FileConfig{ MinSize: 5, MaxSize: 100, })(handler) @@ -112,4 +124,4 @@ func TestFileSizeCheck(t *testing.T) { assert.NoError(t, err) assert.Equal(t, http.StatusOK, rec.Code) }) -} \ No newline at end of file +} \ No newline at end of file diff --git a/backend/internal/middleware/file_size_check.go b/backend/internal/middleware/file_size_check.go deleted file mode 100644 index 25b22434..00000000 --- a/backend/internal/middleware/file_size_check.go +++ /dev/null @@ -1,80 +0,0 @@ -package middleware - -import ( - "fmt" - "net/http" - - "github.com/labstack/echo/v4" -) - -/* -FileSizeConfig holds configuration for the file size check middleware. -MinSize and MaxSize are in bytes. - -Example: - 1MB = 1 * 1024 * 1024 bytes - 10MB = 10 * 1024 * 1024 bytes -*/ -type FileSizeConfig struct { - MinSize int64 - MaxSize int64 -} - -/* -FileSizeCheck middleware ensures uploaded files are within specified size limits. -It checks the Content-Length header and returns 413 if file is too large -or 400 if file is too small. - -Usage: - e.POST("/upload", handler, middleware.FileSizeCheck(middleware.FileSizeConfig{ - MinSize: 1024, // 1KB minimum - MaxSize: 10485760, // 10MB maximum - })) -*/ -func FileSizeCheck(config FileSizeConfig) echo.MiddlewareFunc { - return func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - // only check on requests that might have file uploads - if c.Request().Method != http.MethodPost && c.Request().Method != http.MethodPut { - return next(c) - } - - // first check content-length as early rejection - contentLength := c.Request().ContentLength - if contentLength == -1 { - return echo.NewHTTPError(http.StatusBadRequest, "content length required") - } - - if contentLength > config.MaxSize { - return echo.NewHTTPError(http.StatusRequestEntityTooLarge, - fmt.Sprintf("file size %d exceeds maximum allowed size of %d", contentLength, config.MaxSize)) - } - - // parse multipart form with max size limit to prevent memory exhaustion - if err := c.Request().ParseMultipartForm(config.MaxSize); err != nil { - return echo.NewHTTPError(http.StatusRequestEntityTooLarge, "file too large") - } - - // check actual file sizes if the content-length check passed - // (i don't think it would ever happen, but clients can fake a content-length header) - form := c.Request().MultipartForm - if form != nil && form.File != nil { - for _, files := range form.File { - for _, file := range files { - size := file.Size - if size > config.MaxSize { - return echo.NewHTTPError(http.StatusRequestEntityTooLarge, - fmt.Sprintf("file %s size %d exceeds maximum allowed size of %d", file.Filename, size, config.MaxSize)) - } - if size < config.MinSize { - return echo.NewHTTPError(http.StatusBadRequest, - fmt.Sprintf("file %s size %d below minimum required size of %d", file.Filename, size, config.MinSize)) - } - } - } - } - - return next(c) - } - } -} \ No newline at end of file From 16e71d78a9ca4ed5dcbecb1e83dbc4074662e390 Mon Sep 17 00:00:00 2001 From: AmirAgassi <33383085+AmirAgassi@users.noreply.github.com> Date: Wed, 18 Dec 2024 16:10:09 -0500 Subject: [PATCH 4/4] implement hybrid MIME type validation with strict mode option --- backend/internal/middleware/file_check.go | 49 +++++-- .../internal/middleware/file_check_test.go | 131 +++++++++++++++--- 2 files changed, 146 insertions(+), 34 deletions(-) diff --git a/backend/internal/middleware/file_check.go b/backend/internal/middleware/file_check.go index c245da6b..0158f093 100644 --- a/backend/internal/middleware/file_check.go +++ b/backend/internal/middleware/file_check.go @@ -4,6 +4,7 @@ import ( "fmt" "mime/multipart" "net/http" + "strings" "github.com/gabriel-vasile/mimetype" "github.com/labstack/echo/v4" @@ -18,9 +19,10 @@ Example: 10MB = 10 * 1024 * 1024 bytes */ type FileConfig struct { - MinSize int64 - MaxSize int64 - AllowedTypes []string // e.g. ["image/jpeg", "image/png", "application/pdf"] + MinSize int64 + MaxSize int64 + AllowedTypes []string // ex. ["image/jpeg", "image/png", "application/pdf"] + StrictValidation bool // If true, always verify content type matches header } /* @@ -95,20 +97,40 @@ func validateFile(file *multipart.FileHeader, config FileConfig) error { // Check MIME type if restrictions are specified if len(config.AllowedTypes) > 0 { - f, err := file.Open() - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, "could not read file") - } - defer f.Close() + declaredType := file.Header.Get("Content-Type") + declaredType = strings.Split(declaredType, ";")[0] // Remove parameters + + // If no Content-Type header or strict validation is enabled, check actual content + if declaredType == "" || config.StrictValidation { + f, err := file.Open() + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "could not read file") + } + defer f.Close() + + mime, err := mimetype.DetectReader(f) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "could not detect file type") + } - mime, err := mimetype.DetectReader(f) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, "could not detect file type") + actualType := mime.String() + + // If we have both types, verify they match (when strict validation is enabled) + if declaredType != "" && config.StrictValidation && !strings.EqualFold(declaredType, actualType) { + return echo.NewHTTPError(http.StatusBadRequest, + fmt.Sprintf("declared Content-Type (%s) doesn't match actual content type (%s)", + declaredType, actualType)) + } + + // Use actual type if no declared type, otherwise use declared type + if declaredType == "" { + declaredType = actualType + } } isAllowed := false for _, allowed := range config.AllowedTypes { - if mime.Is(allowed) { + if strings.EqualFold(declaredType, allowed) { isAllowed = true break } @@ -116,7 +138,8 @@ func validateFile(file *multipart.FileHeader, config FileConfig) error { if !isAllowed { return echo.NewHTTPError(http.StatusBadRequest, - fmt.Sprintf("file type %s not allowed. Allowed types: %v", mime.String(), config.AllowedTypes)) + fmt.Sprintf("file type %s not allowed for %s. Allowed types: %v", + declaredType, file.Filename, config.AllowedTypes)) } } diff --git a/backend/internal/middleware/file_check_test.go b/backend/internal/middleware/file_check_test.go index 79be75c6..6df11a59 100644 --- a/backend/internal/middleware/file_check_test.go +++ b/backend/internal/middleware/file_check_test.go @@ -2,9 +2,11 @@ package middleware import ( "bytes" + "fmt" "mime/multipart" "net/http" "net/http/httptest" + "net/textproto" "testing" "github.com/labstack/echo/v4" @@ -18,21 +20,44 @@ func TestFileCheck(t *testing.T) { return c.String(http.StatusOK, "success") } - // helper to create multipart request with a file - createMultipartRequest := func(filename string, content []byte) (*http.Request, *bytes.Buffer, error) { + // helper to create multipart request with a file and optional content type + createMultipartRequest := func(filename string, content []byte, contentType string) (*http.Request, error) { body := new(bytes.Buffer) writer := multipart.NewWriter(body) - part, err := writer.CreateFormFile("file", filename) + + // Create form file with headers + h := make(textproto.MIMEHeader) + h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, "file", filename)) + if contentType != "" { + h.Set("Content-Type", contentType) + } + + part, err := writer.CreatePart(h) if err != nil { - return nil, nil, err + return nil, err } + part.Write(content) writer.Close() req := httptest.NewRequest(http.MethodPost, "/", body) req.Header.Set("Content-Type", writer.FormDataContentType()) req.ContentLength = int64(body.Len()) - return req, body, nil + return req, nil + } + + // Sample file contents with proper headers + jpegHeader := []byte{ + 0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, + 0x49, 0x46, 0x00, 0x01, + } + pngHeader := []byte{ + 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, + 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, + } + pdfHeader := []byte{ + 0x25, 0x50, 0x44, 0x46, 0x2D, 0x31, 0x2E, 0x34, + 0x0A, 0x25, 0xC7, 0xEC, 0x8F, 0xA2, 0x0A, } tests := []struct { @@ -40,57 +65,118 @@ func TestFileCheck(t *testing.T) { config FileConfig filename string content []byte + contentType string expectedStatus int + expectedError string }{ { - name: "valid file", + name: "valid jpeg with matching content type", config: FileConfig{ - MinSize: 5, - MaxSize: 1024, - AllowedTypes: []string{"text/plain"}, + MinSize: 3, + MaxSize: 1024, + AllowedTypes: []string{"image/jpeg"}, + StrictValidation: true, + }, + filename: "test.jpg", + content: append(jpegHeader, []byte("dummy content")...), + contentType: "image/jpeg", + expectedStatus: http.StatusOK, + }, + { + name: "valid png without content type header", + config: FileConfig{ + MinSize: 4, + MaxSize: 1024, + AllowedTypes: []string{"image/png"}, + StrictValidation: false, }, - filename: "test.txt", - content: []byte("Hello, World!"), + filename: "test.png", + content: append(pngHeader, []byte("dummy content")...), expectedStatus: http.StatusOK, }, + { + name: "mismatched content type with strict validation", + config: FileConfig{ + MinSize: 5, + MaxSize: 1024, + AllowedTypes: []string{"image/jpeg", "image/png"}, + StrictValidation: true, + }, + filename: "test.jpg", + content: append(pngHeader, []byte("dummy content")...), + contentType: "image/jpeg", + expectedStatus: http.StatusBadRequest, + expectedError: "doesn't match actual content type", + }, { name: "file too large", config: FileConfig{ MinSize: 5, MaxSize: 100, - AllowedTypes: []string{"text/plain"}, + AllowedTypes: []string{"image/jpeg"}, }, - filename: "large.txt", - content: bytes.Repeat([]byte("a"), 150), + filename: "large.jpg", + content: append(jpegHeader, bytes.Repeat([]byte("a"), 150)...), + contentType: "image/jpeg", expectedStatus: http.StatusRequestEntityTooLarge, + expectedError: "file size", }, { name: "file too small", config: FileConfig{ MinSize: 50, MaxSize: 1024, - AllowedTypes: []string{"text/plain"}, + AllowedTypes: []string{"image/jpeg"}, }, - filename: "small.txt", - content: []byte("tiny"), + filename: "small.jpg", + content: append(jpegHeader, []byte("tiny")...), + contentType: "image/jpeg", expectedStatus: http.StatusBadRequest, + expectedError: "below minimum required size", }, { - name: "invalid mime type", + name: "wrong mime type", config: FileConfig{ MinSize: 5, MaxSize: 1024, AllowedTypes: []string{"image/jpeg", "image/png"}, }, - filename: "test.txt", - content: []byte("Hello, World!"), + filename: "document.pdf", + content: append(pdfHeader, []byte("dummy content")...), + contentType: "application/pdf", expectedStatus: http.StatusBadRequest, + expectedError: "file type", + }, + { + name: "multiple allowed types", + config: FileConfig{ + MinSize: 5, + MaxSize: 1024, + AllowedTypes: []string{"image/jpeg", "image/png", "application/pdf"}, + }, + filename: "document.pdf", + content: append(pdfHeader, []byte("dummy content")...), + contentType: "application/pdf", + expectedStatus: http.StatusOK, + }, + { + name: "strict validation success", + config: FileConfig{ + MinSize: 5, + MaxSize: 1024, + AllowedTypes: []string{"application/pdf"}, + StrictValidation: true, + }, + filename: "document.pdf", + content: append(pdfHeader, []byte("dummy content")...), + contentType: "application/pdf", + expectedStatus: http.StatusOK, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - req, _, err := createMultipartRequest(tt.filename, tt.content) + req, err := createMultipartRequest(tt.filename, tt.content, tt.contentType) assert.NoError(t, err) rec := httptest.NewRecorder() @@ -103,6 +189,9 @@ func TestFileCheck(t *testing.T) { he, ok := err.(*echo.HTTPError) assert.True(t, ok) assert.Equal(t, tt.expectedStatus, he.Code) + if tt.expectedError != "" { + assert.Contains(t, he.Message, tt.expectedError) + } } else { assert.NoError(t, err) }