Files
RenewCMS/test/image_variants_domain_test.go
2026-10-08 02:33:32 +02:00

243 lines
8.2 KiB
Go

package test
import (
domain "RenewCMS/internal/domain/image"
"bytes"
"errors"
"strings"
"testing"
"time"
)
func TestVariantNames(t *testing.T) {
rows := []struct {
name string
valid bool
}{
{"ab", true},
{"a-b-c", true},
{"q1", true},
{strings.Repeat("a", 32), true},
{strings.Repeat("a", 33), false},
{"a", false},
{"", false},
{"Hero", false},
{"he ro", false},
{"he_ro", false},
{"hé", false},
}
for _, row := range rows {
t.Run("name "+row.name, func(t *testing.T) {
if got := domain.IsValidVariantName(row.name); got != row.valid {
t.Errorf("valid = %v", got)
}
})
}
}
func TestVariantSettingsNormalization(t *testing.T) {
settings := func(width *int, height *int, quality *int, format domain.VariantFormat) domain.VariantSettings {
return domain.VariantSettings{Width: width, Height: height, Quality: quality, Format: format}
}
valid := []struct {
name string
in domain.VariantSettings
}{
{"width 1", settings(intPointer(1), nil, nil, "")},
{"width 8000", settings(intPointer(8000), nil, nil, "")},
{"height 1", settings(nil, intPointer(1), nil, "")},
{"height 8000", settings(nil, intPointer(8000), nil, "")},
{"quality 1", settings(intPointer(5), nil, intPointer(1), "")},
{"quality 100", settings(intPointer(5), nil, intPointer(100), "")},
}
for _, row := range valid {
t.Run("accepts "+row.name, func(t *testing.T) {
if _, err := row.in.Normalized(); err != nil {
t.Errorf("err = %v", err)
}
})
}
invalid := []struct {
name string
in domain.VariantSettings
}{
{"no dimension", settings(nil, nil, nil, "")},
{"width 0", settings(intPointer(0), nil, nil, "")},
{"width 8001", settings(intPointer(8001), nil, nil, "")},
{"height 0", settings(nil, intPointer(0), nil, "")},
{"height 8001", settings(nil, intPointer(8001), nil, "")},
{"height negative", settings(intPointer(10), intPointer(-1), nil, "")},
{"quality 0", settings(intPointer(5), nil, intPointer(0), "")},
{"quality 101", settings(intPointer(5), nil, intPointer(101), "")},
{"unknown format", settings(intPointer(5), nil, nil, "avif")},
{"unknown fit", domain.VariantSettings{Width: intPointer(5), Fit: "stretch"}},
}
for _, row := range invalid {
t.Run("refuses "+row.name, func(t *testing.T) {
if _, err := row.in.Normalized(); !errors.Is(err, domain.ErrVariantInvalid) {
t.Errorf("err = %v", err)
}
})
}
t.Run("Defaults are cover, original and quality 82", func(t *testing.T) {
normalized, err := settings(intPointer(400), nil, nil, "").Normalized()
if err != nil || normalized.Fit != domain.FitCover || normalized.Format != domain.FormatOriginal || *normalized.Quality != 82 {
t.Errorf("normalized = %+v, err = %v", normalized, err)
}
})
t.Run("A png variant stores no quality", func(t *testing.T) {
normalized, _ := settings(intPointer(400), nil, intPointer(30), domain.FormatPNG).Normalized()
if normalized.Quality != nil {
t.Errorf("quality = %v", *normalized.Quality)
}
})
t.Run("Normalizing does not alter the input", func(t *testing.T) {
quality := 40
input := settings(intPointer(400), nil, &quality, domain.FormatPNG)
_, _ = input.Normalized()
if input.Quality == nil || *input.Quality != 40 {
t.Error("the input quality was cleared")
}
})
}
func TestVariantFingerprint(t *testing.T) {
base := domain.VariantSettings{Width: intPointer(400), Height: intPointer(300), Fit: domain.FitCover, Format: domain.FormatJPEG, Quality: intPointer(80)}
variations := map[string]domain.VariantSettings{
"width": {Width: intPointer(401), Height: intPointer(300), Fit: domain.FitCover, Format: domain.FormatJPEG, Quality: intPointer(80)},
"height": {Width: intPointer(400), Height: nil, Fit: domain.FitCover, Format: domain.FormatJPEG, Quality: intPointer(80)},
"fit": {Width: intPointer(400), Height: intPointer(300), Fit: domain.FitInside, Format: domain.FormatJPEG, Quality: intPointer(80)},
"format": {Width: intPointer(400), Height: intPointer(300), Fit: domain.FitCover, Format: domain.FormatWebP, Quality: intPointer(80)},
"quality": {Width: intPointer(400), Height: intPointer(300), Fit: domain.FitCover, Format: domain.FormatJPEG, Quality: intPointer(79)},
}
t.Run("Equal settings share a fingerprint", func(t *testing.T) {
same := domain.VariantSettings{Width: intPointer(400), Height: intPointer(300), Fit: domain.FitCover, Format: domain.FormatJPEG, Quality: intPointer(80)}
if !base.IsSameAs(same) {
t.Error("fingerprints differ")
}
})
for field, changed := range variations {
t.Run("A different "+field+" changes the fingerprint", func(t *testing.T) {
if base.IsSameAs(changed) {
t.Error("fingerprints match")
}
})
}
t.Run("An absent dimension differs from a present one", func(t *testing.T) {
widthOnly := domain.VariantSettings{Width: intPointer(300)}
heightOnly := domain.VariantSettings{Height: intPointer(300)}
if widthOnly.IsSameAs(heightOnly) {
t.Error("fingerprints match")
}
})
}
func TestVariantConstruction(t *testing.T) {
now := time.Date(2026, 10, 8, 9, 0, 0, 0, time.UTC)
t.Run("A new variant carries the normalized settings and timestamps", func(t *testing.T) {
variant, err := domain.NewVariant("hero", domain.VariantSettings{Width: intPointer(100), Format: domain.FormatPNG, Quality: intPointer(5)}, true, false, now)
if err != nil || variant.Quality != nil || variant.Fit != domain.FitCover || !variant.GenerateOnUpload || variant.Enabled || !variant.CreatedAt.Equal(now) || !variant.UpdatedAt.Equal(now) {
t.Errorf("variant = %+v, err = %v", variant, err)
}
})
t.Run("A new variant refuses an invalid name", func(t *testing.T) {
_, err := domain.NewVariant("A", domain.VariantSettings{Width: intPointer(100)}, false, true, now)
if !errors.Is(err, domain.ErrVariantInvalid) {
t.Errorf("err = %v", err)
}
})
}
func TestVariantETagAndRequestErrors(t *testing.T) {
settings := domain.VariantSettings{Width: intPointer(400), Fit: domain.FitCover, Format: domain.FormatJPEG, Quality: intPointer(80)}
t.Run("The ETag is quoted and depends on the source", func(t *testing.T) {
first, second := domain.VariantETag(settings, "a.png"), domain.VariantETag(settings, "b.png")
if !strings.HasPrefix(first, `"`) || !strings.HasSuffix(first, `"`) || first == second || first != domain.VariantETag(settings, "a.png") {
t.Errorf("etags = %q %q", first, second)
}
})
matches := []struct {
header string
want bool
}{
{`"abc"`, true},
{`W/"abc"`, true},
{`"x", "abc"`, true},
{`*`, true},
{`"abd"`, false},
{``, false},
}
for _, row := range matches {
t.Run("If-None-Match "+row.header, func(t *testing.T) {
if got := domain.ETagMatches(row.header, `"abc"`); got != row.want {
t.Errorf("matches = %v", got)
}
})
}
t.Run("Request errors unwrap to their sentinel and carry the detail", func(t *testing.T) {
unknown := domain.NewUnknownVariantError("banner")
unsupported := domain.NewUnsupportedParameterError("width")
if !errors.Is(unknown, domain.ErrUnknownVariant) || errors.Is(unknown, domain.ErrUnsupportedParameter) || domain.VariantRequestDetail(unknown) != "banner" {
t.Errorf("unknown = %v", unknown)
}
if !errors.Is(unsupported, domain.ErrUnsupportedParameter) || domain.VariantRequestDetail(unsupported) != "width" || domain.VariantRequestDetail(errors.New("other")) != "" {
t.Errorf("unsupported = %v", unsupported)
}
})
}
func TestAnimatedGIFDetection(t *testing.T) {
t.Run("A two-frame GIF is animated", func(t *testing.T) {
animated, err := domain.IsAnimatedGIF(bytes.NewReader(animatedGIF(t)))
if err != nil || !animated {
t.Errorf("animated = %v, err = %v", animated, err)
}
})
t.Run("A one-frame GIF is not animated", func(t *testing.T) {
animated, err := domain.IsAnimatedGIF(bytes.NewReader(stillGIF(t)))
if err != nil || animated {
t.Errorf("animated = %v, err = %v", animated, err)
}
})
t.Run("Bytes that are not a GIF are refused", func(t *testing.T) {
_, err := domain.IsAnimatedGIF(bytes.NewReader(solidPNG(t, 4, 4)))
if !errors.Is(err, domain.ErrNotAGif) {
t.Errorf("err = %v", err)
}
})
t.Run("A truncated GIF reports an error", func(t *testing.T) {
truncated := animatedGIF(t)[:20]
if _, err := domain.IsAnimatedGIF(bytes.NewReader(truncated)); err == nil {
t.Error("expected an error")
}
})
}