package test import ( domain "RenewCMS/internal/domain/image" "RenewCMS/internal/infrastructure/imaging" "RenewCMS/internal/infrastructure/useCases" "bytes" "errors" "io" "log" "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 totalsBySourceCalls 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) TotalsBySource() (map[string]domain.VariantFileTotals, error) { m.mutex.Lock() defer m.mutex.Unlock() m.calls++ m.totalsBySourceCalls++ totals := map[string]domain.VariantFileTotals{} for _, row := range m.rows { current := totals[row.SourceName] totals[row.SourceName] = 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() failFor func(settings domain.VariantSettings) error } 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.failFor != nil { if err := c.failFor(settings); err != nil { return domain.RenderedVariant{}, err } } 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 newInertGenerateOnUploadUseCase() *useCases.GenerateVariantsOnUploadUseCase { return useCases.NewGenerateVariantsOnUploadUseCase(newMockVariantRepo(), newInertServeVariantUseCase(newMockVariantStorage()), log.New(io.Discard, "", 0)) } func (m *mockVariantStorage) openCountOf(storedName string) int { m.mutex.Lock() defer m.mutex.Unlock() return m.openCounts[storedName] }