379 lines
9.9 KiB
Go
379 lines
9.9 KiB
Go
package test
|
|
|
|
import (
|
|
domain "RenewCMS/internal/domain/image"
|
|
"RenewCMS/internal/infrastructure/imaging"
|
|
"RenewCMS/internal/infrastructure/useCases"
|
|
"bytes"
|
|
"errors"
|
|
"io"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
)
|
|
|
|
var errVariantStorageFailure = errors.New("variant storage failure")
|
|
|
|
var variantStorageExtensions = map[string]string{
|
|
"image/png": ".png",
|
|
"image/jpeg": ".jpeg",
|
|
"image/webp": ".webp",
|
|
}
|
|
|
|
type mockVariantRepo struct {
|
|
mutex sync.Mutex
|
|
variants map[string]domain.Variant
|
|
failGet error
|
|
calls int
|
|
}
|
|
|
|
var _ domain.VariantRepository = &mockVariantRepo{}
|
|
|
|
func newMockVariantRepo() *mockVariantRepo {
|
|
return &mockVariantRepo{variants: map[string]domain.Variant{}}
|
|
}
|
|
|
|
func (m *mockVariantRepo) List() ([]domain.Variant, error) {
|
|
m.mutex.Lock()
|
|
defer m.mutex.Unlock()
|
|
m.calls++
|
|
listed := make([]domain.Variant, 0, len(m.variants))
|
|
for _, variant := range m.variants {
|
|
listed = append(listed, variant)
|
|
}
|
|
sort.Slice(listed, func(i, j int) bool { return listed[i].Name < listed[j].Name })
|
|
return listed, nil
|
|
}
|
|
|
|
func (m *mockVariantRepo) Get(name string) (domain.Variant, error) {
|
|
m.mutex.Lock()
|
|
defer m.mutex.Unlock()
|
|
m.calls++
|
|
if m.failGet != nil {
|
|
return domain.Variant{}, m.failGet
|
|
}
|
|
variant, found := m.variants[name]
|
|
if !found {
|
|
return domain.Variant{}, domain.ErrVariantNotFound
|
|
}
|
|
return variant, nil
|
|
}
|
|
|
|
func (m *mockVariantRepo) Create(variant domain.Variant) (domain.Variant, error) {
|
|
m.mutex.Lock()
|
|
defer m.mutex.Unlock()
|
|
m.calls++
|
|
if _, found := m.variants[variant.Name]; found {
|
|
return domain.Variant{}, domain.ErrVariantExists
|
|
}
|
|
m.variants[variant.Name] = variant
|
|
return variant, nil
|
|
}
|
|
|
|
func (m *mockVariantRepo) Update(variant domain.Variant) (domain.Variant, error) {
|
|
m.mutex.Lock()
|
|
defer m.mutex.Unlock()
|
|
m.calls++
|
|
if _, found := m.variants[variant.Name]; !found {
|
|
return domain.Variant{}, domain.ErrVariantNotFound
|
|
}
|
|
m.variants[variant.Name] = variant
|
|
return variant, nil
|
|
}
|
|
|
|
func (m *mockVariantRepo) Delete(name string) error {
|
|
m.mutex.Lock()
|
|
defer m.mutex.Unlock()
|
|
m.calls++
|
|
if _, found := m.variants[name]; !found {
|
|
return domain.ErrVariantNotFound
|
|
}
|
|
delete(m.variants, name)
|
|
return nil
|
|
}
|
|
|
|
func (m *mockVariantRepo) stored(name string) domain.Variant {
|
|
m.mutex.Lock()
|
|
defer m.mutex.Unlock()
|
|
return m.variants[name]
|
|
}
|
|
|
|
func (m *mockVariantRepo) count() int {
|
|
m.mutex.Lock()
|
|
defer m.mutex.Unlock()
|
|
return len(m.variants)
|
|
}
|
|
|
|
type mockVariantFileRepo struct {
|
|
mutex sync.Mutex
|
|
rows map[uint32]domain.VariantFile
|
|
nextID uint32
|
|
deleted []uint32
|
|
saves int
|
|
calls int
|
|
}
|
|
|
|
var _ domain.VariantFileRepository = &mockVariantFileRepo{}
|
|
|
|
func newMockVariantFileRepo() *mockVariantFileRepo {
|
|
return &mockVariantFileRepo{rows: map[uint32]domain.VariantFile{}}
|
|
}
|
|
|
|
func (m *mockVariantFileRepo) Find(variantName string, sourceName string) (domain.VariantFile, bool, error) {
|
|
m.mutex.Lock()
|
|
defer m.mutex.Unlock()
|
|
m.calls++
|
|
for _, row := range m.rows {
|
|
if row.VariantName == variantName && row.SourceName == sourceName {
|
|
return row, true, nil
|
|
}
|
|
}
|
|
return domain.VariantFile{}, false, nil
|
|
}
|
|
|
|
func (m *mockVariantFileRepo) Save(file domain.VariantFile) (domain.VariantFile, error) {
|
|
m.mutex.Lock()
|
|
defer m.mutex.Unlock()
|
|
m.calls++
|
|
m.saves++
|
|
for id, row := range m.rows {
|
|
if row.VariantName == file.VariantName && row.SourceName == file.SourceName {
|
|
file.ID = id
|
|
m.rows[id] = file
|
|
return file, nil
|
|
}
|
|
}
|
|
m.nextID++
|
|
file.ID = m.nextID
|
|
m.rows[file.ID] = file
|
|
return file, nil
|
|
}
|
|
|
|
func (m *mockVariantFileRepo) ListByVariant(variantName string) ([]domain.VariantFile, error) {
|
|
return m.filtered(func(row domain.VariantFile) bool { return row.VariantName == variantName }), nil
|
|
}
|
|
|
|
func (m *mockVariantFileRepo) ListBySource(sourceName string) ([]domain.VariantFile, error) {
|
|
return m.filtered(func(row domain.VariantFile) bool { return row.SourceName == sourceName }), nil
|
|
}
|
|
|
|
func (m *mockVariantFileRepo) filtered(keep func(domain.VariantFile) bool) []domain.VariantFile {
|
|
m.mutex.Lock()
|
|
defer m.mutex.Unlock()
|
|
m.calls++
|
|
kept := []domain.VariantFile{}
|
|
for _, row := range m.rows {
|
|
if keep(row) {
|
|
kept = append(kept, row)
|
|
}
|
|
}
|
|
sort.Slice(kept, func(i, j int) bool { return kept[i].ID < kept[j].ID })
|
|
return kept
|
|
}
|
|
|
|
func (m *mockVariantFileRepo) Delete(id uint32) error {
|
|
m.mutex.Lock()
|
|
defer m.mutex.Unlock()
|
|
m.calls++
|
|
m.deleted = append(m.deleted, id)
|
|
delete(m.rows, id)
|
|
return nil
|
|
}
|
|
|
|
func (m *mockVariantFileRepo) Totals() (map[string]domain.VariantFileTotals, error) {
|
|
m.mutex.Lock()
|
|
defer m.mutex.Unlock()
|
|
m.calls++
|
|
totals := map[string]domain.VariantFileTotals{}
|
|
for _, row := range m.rows {
|
|
current := totals[row.VariantName]
|
|
totals[row.VariantName] = domain.VariantFileTotals{Files: current.Files + 1, Bytes: current.Bytes + row.Size}
|
|
}
|
|
return totals, nil
|
|
}
|
|
|
|
func (m *mockVariantFileRepo) rowsOf(variantName string) []domain.VariantFile {
|
|
rows, _ := m.ListByVariant(variantName)
|
|
return rows
|
|
}
|
|
|
|
func (m *mockVariantFileRepo) rowCount() int {
|
|
m.mutex.Lock()
|
|
defer m.mutex.Unlock()
|
|
return len(m.rows)
|
|
}
|
|
|
|
type mockVariantStorage struct {
|
|
mutex sync.Mutex
|
|
blobs map[string][]byte
|
|
puts int
|
|
opens int
|
|
openCounts map[string]int
|
|
nextName int
|
|
deleted []string
|
|
failDelete map[string]error
|
|
}
|
|
|
|
var _ domain.Storage = &mockVariantStorage{}
|
|
|
|
func newMockVariantStorage() *mockVariantStorage {
|
|
return &mockVariantStorage{blobs: map[string][]byte{}, failDelete: map[string]error{}, openCounts: map[string]int{}}
|
|
}
|
|
|
|
func (m *mockVariantStorage) Put(contentType string, content io.Reader) (string, error) {
|
|
data, err := io.ReadAll(content)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
m.mutex.Lock()
|
|
defer m.mutex.Unlock()
|
|
m.puts++
|
|
m.nextName++
|
|
name := "gen-" + strconv.Itoa(m.nextName) + variantStorageExtensions[contentType]
|
|
m.blobs[name] = data
|
|
return name, nil
|
|
}
|
|
|
|
func (m *mockVariantStorage) Open(storedName string) (domain.Blob, error) {
|
|
m.mutex.Lock()
|
|
defer m.mutex.Unlock()
|
|
m.opens++
|
|
m.openCounts[storedName]++
|
|
data, found := m.blobs[storedName]
|
|
if !found || strings.Contains(storedName, "/") {
|
|
return domain.Blob{}, domain.ErrBlobNotFound
|
|
}
|
|
return domain.Blob{Content: io.NopCloser(bytes.NewReader(data)), Size: int64(len(data)), ModTime: time.Date(2026, 10, 8, 9, 0, 0, 0, time.UTC)}, nil
|
|
}
|
|
|
|
func (m *mockVariantStorage) Delete(storedName string) error {
|
|
m.mutex.Lock()
|
|
defer m.mutex.Unlock()
|
|
m.deleted = append(m.deleted, storedName)
|
|
if err := m.failDelete[storedName]; err != nil {
|
|
return err
|
|
}
|
|
delete(m.blobs, storedName)
|
|
return nil
|
|
}
|
|
|
|
func (m *mockVariantStorage) seed(storedName string, content []byte) {
|
|
m.mutex.Lock()
|
|
defer m.mutex.Unlock()
|
|
m.blobs[storedName] = content
|
|
}
|
|
|
|
func (m *mockVariantStorage) has(storedName string) bool {
|
|
m.mutex.Lock()
|
|
defer m.mutex.Unlock()
|
|
_, found := m.blobs[storedName]
|
|
return found
|
|
}
|
|
|
|
func (m *mockVariantStorage) putCount() int {
|
|
m.mutex.Lock()
|
|
defer m.mutex.Unlock()
|
|
return m.puts
|
|
}
|
|
|
|
func (m *mockVariantStorage) deletedNames() []string {
|
|
m.mutex.Lock()
|
|
defer m.mutex.Unlock()
|
|
return append([]string(nil), m.deleted...)
|
|
}
|
|
|
|
type countingRenderer struct {
|
|
inner domain.VariantRenderer
|
|
calls atomic.Int32
|
|
failFirst bool
|
|
onRender func()
|
|
}
|
|
|
|
var _ domain.VariantRenderer = &countingRenderer{}
|
|
|
|
func newCountingRenderer() *countingRenderer {
|
|
return &countingRenderer{inner: imaging.NewVariantRenderer()}
|
|
}
|
|
|
|
func newCountingRendererWithoutWebP() *countingRenderer {
|
|
return &countingRenderer{inner: imaging.NewVariantRendererEncoding(domain.FormatOriginal, domain.FormatJPEG, domain.FormatPNG)}
|
|
}
|
|
|
|
func (c *countingRenderer) Render(source io.Reader, settings domain.VariantSettings) (domain.RenderedVariant, error) {
|
|
call := c.calls.Add(1)
|
|
if c.onRender != nil {
|
|
c.onRender()
|
|
}
|
|
if c.failFirst && call == 1 {
|
|
return domain.RenderedVariant{}, errors.New("render failure")
|
|
}
|
|
return c.inner.Render(source, settings)
|
|
}
|
|
|
|
func (c *countingRenderer) DeclaredSize(source io.Reader) (int, int, error) {
|
|
return c.inner.DeclaredSize(source)
|
|
}
|
|
|
|
func defaultServeLimits(maxSourceBytes int64) useCases.ServeVariantLimits {
|
|
return useCases.ServeVariantLimits{MaxSourceBytes: maxSourceBytes, MaxSourcePixels: variantDefaultPixels, MaxConcurrentRenders: variantDefaultRenders}
|
|
}
|
|
|
|
func (c *countingRenderer) EncodableFormats() []domain.VariantFormat {
|
|
return c.inner.EncodableFormats()
|
|
}
|
|
|
|
func (c *countingRenderer) renderCount() int {
|
|
return int(c.calls.Load())
|
|
}
|
|
|
|
func seedVariant(repo *mockVariantRepo, name string, width int, height int, fit domain.VariantFit, format domain.VariantFormat, quality int) domain.Variant {
|
|
settings := domain.VariantSettings{Fit: fit, Format: format}
|
|
if width > 0 {
|
|
settings.Width = &width
|
|
}
|
|
if height > 0 {
|
|
settings.Height = &height
|
|
}
|
|
if format != domain.FormatPNG {
|
|
settings.Quality = &quality
|
|
}
|
|
stamp := time.Date(2026, 10, 7, 8, 0, 0, 0, time.UTC)
|
|
variant, err := domain.NewVariant(name, settings, false, true, stamp)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
repo.variants[name] = variant
|
|
return variant
|
|
}
|
|
|
|
func seedVariantFiles(files *mockVariantFileRepo, storage *mockVariantStorage, variantName string, sourceName string, count int, size int64) []domain.VariantFile {
|
|
seeded := []domain.VariantFile{}
|
|
for index := 0; index < count; index++ {
|
|
cached := "seed-" + variantName + "-" + sourceName + "-" + strconv.Itoa(index) + ".jpeg"
|
|
storage.seed(cached, []byte("generated"))
|
|
saved, _ := files.Save(domain.VariantFile{
|
|
VariantName: variantName,
|
|
SourceName: sourceName + "-" + strconv.Itoa(index),
|
|
CachedName: cached,
|
|
ContentType: "image/jpeg",
|
|
Size: size,
|
|
Fingerprint: "seeded",
|
|
})
|
|
seeded = append(seeded, saved)
|
|
}
|
|
return seeded
|
|
}
|
|
|
|
func newInertServeVariantUseCase(storage domain.Storage) *useCases.ServeVariantUseCase {
|
|
return useCases.NewServeVariantUseCase(newMockVariantRepo(), newMockVariantFileRepo(), newMockImageRepo(), storage, newCountingRenderer(), defaultServeLimits(variantDefaultCap))
|
|
}
|
|
|
|
func (m *mockVariantStorage) openCountOf(storedName string) int {
|
|
m.mutex.Lock()
|
|
defer m.mutex.Unlock()
|
|
return m.openCounts[storedName]
|
|
}
|