243 lines
8.2 KiB
Go
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")
|
|
}
|
|
})
|
|
}
|