234 lines
6.4 KiB
Go
234 lines
6.4 KiB
Go
package test
|
|
|
|
import (
|
|
domain "RenewCMS/internal/domain/image"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
var errUsageStoreDown = errors.New("usage store down")
|
|
|
|
type mockVariantUsageRepo struct {
|
|
mutex sync.Mutex
|
|
batches []domain.VariantUsageBatch
|
|
days map[string]int
|
|
fail error
|
|
calls int
|
|
}
|
|
|
|
var _ domain.VariantUsageRepository = &mockVariantUsageRepo{}
|
|
|
|
func newMockVariantUsageRepo() *mockVariantUsageRepo {
|
|
return &mockVariantUsageRepo{days: map[string]int{}}
|
|
}
|
|
|
|
func (m *mockVariantUsageRepo) Record(batch domain.VariantUsageBatch) error {
|
|
m.mutex.Lock()
|
|
defer m.mutex.Unlock()
|
|
m.calls++
|
|
if m.fail != nil {
|
|
return m.fail
|
|
}
|
|
m.batches = append(m.batches, batch)
|
|
for _, rejection := range batch.Rejections {
|
|
m.days[rejection.Day.Format(domain.RejectionDayLayout)] += rejection.Count
|
|
}
|
|
if !batch.PruneBefore.IsZero() {
|
|
for day := range m.days {
|
|
if day < batch.PruneBefore.Format(domain.RejectionDayLayout) {
|
|
delete(m.days, day)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *mockVariantUsageRepo) RejectedSince(day time.Time) (int, error) {
|
|
m.mutex.Lock()
|
|
defer m.mutex.Unlock()
|
|
total := 0
|
|
for stored, count := range m.days {
|
|
if stored >= day.Format(domain.RejectionDayLayout) {
|
|
total += count
|
|
}
|
|
}
|
|
return total, nil
|
|
}
|
|
|
|
func (m *mockVariantUsageRepo) recordedHits() []domain.VariantHit {
|
|
m.mutex.Lock()
|
|
defer m.mutex.Unlock()
|
|
hits := []domain.VariantHit{}
|
|
for _, batch := range m.batches {
|
|
hits = append(hits, batch.Hits...)
|
|
}
|
|
return hits
|
|
}
|
|
|
|
func (m *mockVariantUsageRepo) recordedRejections() int {
|
|
m.mutex.Lock()
|
|
defer m.mutex.Unlock()
|
|
total := 0
|
|
for _, batch := range m.batches {
|
|
for _, rejection := range batch.Rejections {
|
|
total += rejection.Count
|
|
}
|
|
}
|
|
return total
|
|
}
|
|
|
|
func (m *mockVariantUsageRepo) batchCount() int {
|
|
m.mutex.Lock()
|
|
defer m.mutex.Unlock()
|
|
return len(m.batches)
|
|
}
|
|
|
|
func (m *mockVariantUsageRepo) callCount() int {
|
|
m.mutex.Lock()
|
|
defer m.mutex.Unlock()
|
|
return m.calls
|
|
}
|
|
|
|
type cacheFixtureImage struct {
|
|
id uint32
|
|
filename string
|
|
files int
|
|
bytes int64
|
|
}
|
|
|
|
var cacheFixtureTopImages = []cacheFixtureImage{
|
|
{1, "north-wall.jpg", 6, 6082765},
|
|
{2, "lime-plaster.jpg", 6, 5138022},
|
|
{3, "kitchen-after.webp", 5, 4613734},
|
|
{4, "kitchen-before.webp", 5, 4089446},
|
|
{5, "floorboards.jpg", 5, 3774873},
|
|
{6, "IMG_4471.jpg", 4, 3250585},
|
|
{7, "stone-wall-detail.jpg", 4, 2936012},
|
|
{8, "bathroom-tiles.png", 4, 2726297},
|
|
{9, "roof-lantern.jpg", 3, 2411724},
|
|
{10, "chimney-stack.jpg", 3, 2202009},
|
|
}
|
|
|
|
type cacheFixtureVariant struct {
|
|
name string
|
|
enabled bool
|
|
files int
|
|
bytes int64
|
|
}
|
|
|
|
var cacheFixtureVariants = []cacheFixtureVariant{
|
|
{"hero", true, 244, 88080384},
|
|
{"card", true, 318, 60817408},
|
|
{"thumb", true, 412, 22020096},
|
|
{"print", false, 74, 22020096},
|
|
{"square", true, 140, 19922944},
|
|
{"og", true, 96, 9437184},
|
|
}
|
|
|
|
const (
|
|
cacheFixtureUnusedHeroRows = 200
|
|
cacheFixtureUnusedHeroBytes = 40000000
|
|
cacheFixtureUnusedThumbRows = 112
|
|
cacheFixtureUnusedThumbSize = 2991616
|
|
)
|
|
|
|
func splitBytes(total int64, parts int) []int64 {
|
|
sizes := make([]int64, parts)
|
|
for index := range sizes {
|
|
sizes[index] = total / int64(parts)
|
|
if int64(index) < total%int64(parts) {
|
|
sizes[index]++
|
|
}
|
|
}
|
|
return sizes
|
|
}
|
|
|
|
func (e *variantEnv) seedCacheRow(variantName string, sourceName string, size int64, reference time.Time) domain.VariantFile {
|
|
cached := "fixture-" + variantName + "-" + sourceName + ".jpeg"
|
|
e.storage.seed(cached, []byte("generated"))
|
|
saved, _ := e.files.Save(domain.VariantFile{VariantName: variantName, SourceName: sourceName, CachedName: cached, ContentType: "image/jpeg", Size: size, Fingerprint: "fixture"})
|
|
e.files.setReference(saved.ID, reference)
|
|
return saved
|
|
}
|
|
|
|
func (e *variantEnv) seedDefaultCache() {
|
|
recent := e.now.AddDate(0, 0, -5)
|
|
stale := e.now.AddDate(0, 0, -31)
|
|
for _, variant := range cacheFixtureVariants {
|
|
seeded := seedVariant(e.variants, variant.name, 400, 300, domain.FitCover, domain.FormatJPEG, 80)
|
|
seeded.Enabled = variant.enabled
|
|
e.variants.variants[variant.name] = seeded
|
|
}
|
|
topBytes := map[string]int64{}
|
|
topFiles := map[string]int{}
|
|
for _, img := range cacheFixtureTopImages {
|
|
e.seedImage(img.id, img.filename)
|
|
for index, size := range splitBytes(img.bytes, img.files) {
|
|
variant := cacheFixtureVariants[index%len(cacheFixtureVariants)].name
|
|
e.seedCacheRow(variant, img.filename, size, recent)
|
|
topBytes[variant] += size
|
|
topFiles[variant]++
|
|
}
|
|
}
|
|
for _, variant := range cacheFixtureVariants {
|
|
fillers := variant.files - topFiles[variant.name]
|
|
remaining := variant.bytes - topBytes[variant.name]
|
|
sizes := e.fillerSizes(variant.name, fillers, remaining)
|
|
for index, size := range sizes {
|
|
reference := recent
|
|
if e.isFixtureStale(variant.name, index) {
|
|
reference = stale
|
|
}
|
|
e.seedCacheRow(variant.name, "filler-"+variant.name+"-"+strconv.Itoa(index), size, reference)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (e *variantEnv) isFixtureStale(variantName string, index int) bool {
|
|
return (variantName == "hero" && index < cacheFixtureUnusedHeroRows) || (variantName == "thumb" && index < cacheFixtureUnusedThumbRows)
|
|
}
|
|
|
|
func (e *variantEnv) fillerSizes(variantName string, fillers int, remaining int64) []int64 {
|
|
switch variantName {
|
|
case "hero":
|
|
return append(splitBytes(cacheFixtureUnusedHeroBytes, cacheFixtureUnusedHeroRows), splitBytes(remaining-cacheFixtureUnusedHeroBytes, fillers-cacheFixtureUnusedHeroRows)...)
|
|
case "thumb":
|
|
return append(splitBytes(cacheFixtureUnusedThumbSize, cacheFixtureUnusedThumbRows), splitBytes(remaining-cacheFixtureUnusedThumbSize, fillers-cacheFixtureUnusedThumbRows)...)
|
|
default:
|
|
return splitBytes(remaining, fillers)
|
|
}
|
|
}
|
|
|
|
func (e *variantEnv) seedRejectedDays(days map[string]int) {
|
|
for day, count := range days {
|
|
e.usage.days[day] = count
|
|
}
|
|
}
|
|
|
|
func (e *variantEnv) cacheStats() (*httptest.ResponseRecorder, map[string]any) {
|
|
e.t.Helper()
|
|
recorder := e.sendAsAdmin(http.MethodGet, "/v1/image-variants/cache", "")
|
|
if recorder.Code != http.StatusOK {
|
|
return recorder, nil
|
|
}
|
|
return recorder, decodeBody(e.t, recorder)
|
|
}
|
|
|
|
func (e *variantEnv) purgeCache(body string) *httptest.ResponseRecorder {
|
|
e.t.Helper()
|
|
return e.send(http.MethodPost, "/v1/image-variants/cache/purge", body, variantOwnerId)
|
|
}
|
|
|
|
func (e *variantEnv) recentRejectedFixture() {
|
|
e.seedRejectedDays(map[string]int{"2026-10-08": 30, "2026-10-05": 7, "2026-09-20": 100})
|
|
}
|
|
|
|
func compactJSON(body *httptest.ResponseRecorder) string {
|
|
return strings.TrimSpace(body.Body.String())
|
|
}
|