refac: allowed image types and the size limit are a domain rule checked by the use case, not by the persistence layer
This commit is contained in:
1 parent
8a8a24434a
commit
5992c8598b
7 files changed
+249
-17
No files matched your search
@@ -0,0 +1,43 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
imageDomain "RenewCMS/internal/domain/image"
|
||||
"RenewCMS/internal/infrastructure/persistence/models"
|
||||
persistence "RenewCMS/internal/infrastructure/persistence/repositories"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestImageRepositoryHasAnExtensionForEveryAllowedContentType(t *testing.T) {
|
||||
db := openInMemoryDb(t)
|
||||
if err := db.AutoMigrate(&models.Image{}); err != nil {
|
||||
t.Fatalf("failed to migrate: %v", err)
|
||||
}
|
||||
repo := persistence.NewImageRepository(db)
|
||||
t.Setenv("UPLOAD_DIR", t.TempDir())
|
||||
|
||||
allowedContentTypes := imageDomain.AllowedContentTypes()
|
||||
expectedContentTypes := []string{"image/jpeg", "image/png", "image/svg+xml", "image/webp"}
|
||||
if !reflect.DeepEqual(allowedContentTypes, expectedContentTypes) {
|
||||
t.Fatalf("expected AllowedContentTypes() to be %v, got %v", expectedContentTypes, allowedContentTypes)
|
||||
}
|
||||
|
||||
for _, contentType := range allowedContentTypes {
|
||||
t.Run(contentType, func(t *testing.T) {
|
||||
result, err := repo.Create(imageDomain.ImageInput{
|
||||
Content: strings.NewReader("fake-image-data"),
|
||||
Filename: "upload",
|
||||
ContentType: contentType,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Create failed: %v", err)
|
||||
}
|
||||
extension := filepath.Ext(result.Path)
|
||||
if extension == "" {
|
||||
t.Errorf("expected a non-empty extension for %q, got path %q", contentType, result.Path)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -3,21 +3,11 @@ package test
|
||||
import (
|
||||
imageDomain "RenewCMS/internal/domain/image"
|
||||
"RenewCMS/internal/infrastructure/useCases"
|
||||
"errors"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var errUnsupportedImageContentType = errors.New("the file must be a PNG, JPEG, WEBP, or SVG image")
|
||||
|
||||
var supportedImageContentTypes = map[string]bool{
|
||||
"image/png": true,
|
||||
"image/jpeg": true,
|
||||
"image/webp": true,
|
||||
"image/svg+xml": true,
|
||||
}
|
||||
|
||||
type mockImageRepo struct {
|
||||
images map[uint32]imageDomain.Image
|
||||
nextID uint32
|
||||
@@ -34,9 +24,6 @@ func (m *mockImageRepo) Create(input imageDomain.ImageInput) (imageDomain.Image,
|
||||
if err := m.fail.trigger("Create"); err != nil {
|
||||
return imageDomain.Image{}, err
|
||||
}
|
||||
if !supportedImageContentTypes[input.ContentType] {
|
||||
return imageDomain.Image{}, errUnsupportedImageContentType
|
||||
}
|
||||
if input.Content != nil {
|
||||
_, _ = io.ReadAll(input.Content)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
imageDomain "RenewCMS/internal/domain/image"
|
||||
"RenewCMS/internal/infrastructure/useCases"
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestImageUploadRules(t *testing.T) {
|
||||
t.Run("CreateImage accepts every allowed content type within the size limit", func(t *testing.T) {
|
||||
for _, contentType := range []string{"image/png", "image/jpeg", "image/webp", "image/svg+xml"} {
|
||||
t.Run(contentType, func(t *testing.T) {
|
||||
repo := newMockImageRepo()
|
||||
createUc := useCases.NewCreateImageUseCase(repo)
|
||||
|
||||
result, err := createUc.CreateImage(imageDomain.ImageInput{
|
||||
ContentType: contentType,
|
||||
Size: imageDomain.MaxImageSize,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if result.ID == 0 {
|
||||
t.Errorf("expected the created image, got zero value")
|
||||
}
|
||||
if len(repo.images) != 1 {
|
||||
t.Errorf("expected Create to be called once, got %d calls", len(repo.images))
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("CreateImage rejects a disallowed content type before reaching the repository", func(t *testing.T) {
|
||||
repo := newMockImageRepo()
|
||||
createUc := useCases.NewCreateImageUseCase(repo)
|
||||
|
||||
_, err := createUc.CreateImage(imageDomain.ImageInput{
|
||||
ContentType: "application/pdf",
|
||||
Size: 1024,
|
||||
})
|
||||
|
||||
if !errors.Is(err, imageDomain.ErrImageTypeNotAllowed) {
|
||||
t.Fatalf("expected ErrImageTypeNotAllowed, got %v", err)
|
||||
}
|
||||
if len(repo.images) != 0 {
|
||||
t.Errorf("expected Create to never be called, got %d calls", len(repo.images))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("CreateImage rejects a file that exceeds the size limit", func(t *testing.T) {
|
||||
repo := newMockImageRepo()
|
||||
createUc := useCases.NewCreateImageUseCase(repo)
|
||||
|
||||
_, err := createUc.CreateImage(imageDomain.ImageInput{
|
||||
ContentType: "image/png",
|
||||
Size: imageDomain.MaxImageSize + 1,
|
||||
})
|
||||
|
||||
if !errors.Is(err, imageDomain.ErrImageTooLarge) {
|
||||
t.Fatalf("expected ErrImageTooLarge, got %v", err)
|
||||
}
|
||||
if len(repo.images) != 0 {
|
||||
t.Errorf("expected Create to never be called, got %d calls", len(repo.images))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("A file exactly at the size limit is accepted, one byte over is not", func(t *testing.T) {
|
||||
repo := newMockImageRepo()
|
||||
createUc := useCases.NewCreateImageUseCase(repo)
|
||||
|
||||
_, err := createUc.CreateImage(imageDomain.ImageInput{
|
||||
ContentType: "image/png",
|
||||
Size: imageDomain.MaxImageSize,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error at the exact limit, got %v", err)
|
||||
}
|
||||
if len(repo.images) != 1 {
|
||||
t.Fatalf("expected Create to be called once, got %d calls", len(repo.images))
|
||||
}
|
||||
|
||||
_, err = createUc.CreateImage(imageDomain.ImageInput{
|
||||
ContentType: "image/png",
|
||||
Size: imageDomain.MaxImageSize + 1,
|
||||
})
|
||||
if !errors.Is(err, imageDomain.ErrImageTooLarge) {
|
||||
t.Fatalf("expected ErrImageTooLarge, got %v", err)
|
||||
}
|
||||
if len(repo.images) != 1 {
|
||||
t.Errorf("expected Create to still be called exactly once in total, got %d calls", len(repo.images))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("A repository failure unrelated to type or size still propagates unchanged", func(t *testing.T) {
|
||||
repo := newMockImageRepo()
|
||||
repo.fail.failNextWith("Create", errRepositoryFailure)
|
||||
createUc := useCases.NewCreateImageUseCase(repo)
|
||||
|
||||
_, err := createUc.CreateImage(imageDomain.ImageInput{
|
||||
ContentType: "image/png",
|
||||
Size: 1024,
|
||||
})
|
||||
|
||||
if !errors.Is(err, errRepositoryFailure) {
|
||||
t.Fatalf("expected the generic repository error, got %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user