Files
RenewCMS/test/image_variants_per_image_audit_test.go
T

241 lines
9.1 KiB
Go

package test
import (
domain "RenewCMS/internal/domain/image"
"errors"
"net/http"
"testing"
)
const (
tooLargeReason = "The source image is too large to generate this variant."
genericReason = "Something went wrong on our side. Please try again."
)
func (e *variantEnv) seedFiveEnabled() {
seedVariant(e.variants, "card", 800, 0, domain.FitInside, domain.FormatJPEG, 80)
seedVariant(e.variants, "hero", 1200, 630, domain.FitCover, domain.FormatJPEG, 82)
seedVariant(e.variants, "og", 1200, 630, domain.FitCover, domain.FormatJPEG, 80)
seedVariant(e.variants, "square", 600, 600, domain.FitCover, domain.FormatJPEG, 85)
seedVariant(e.variants, "thumb", 400, 300, domain.FitCover, domain.FormatJPEG, 80)
e.seedPerImage(1, "north-wall.jpg", "a1b2.png", "image/png", 1000, 800, solidPNG(e.t, 1000, 800))
}
func (e *variantEnv) failHero(cause error) {
heroFingerprint := e.variants.stored("hero").Settings().Fingerprint()
e.renderer.failFor = func(settings domain.VariantSettings) error {
if settings.Fingerprint() == heroFingerprint {
return cause
}
return nil
}
}
func assertImageTarget(t *testing.T, event map[string]any, id string, name string) {
t.Helper()
target := event["target"].(map[string]any)
if target["type"] != "image" || target["id"] != id || target["name"] != name || event["category"] != "media" {
t.Errorf("target = %v, category = %v", target, event["category"])
}
}
func TestPerImageVariantAudit(t *testing.T) {
t.Run("Generate all is recorded with the counts", func(t *testing.T) {
env := newVariantEnv(t)
env.seedFiveEnabled()
env.send(http.MethodPost, "/v1/media/1/variants", "", variantOwnerId)
action, event := env.lastEvent()
params := event["params"].(map[string]any)
causes, _ := params["causes"].([]any)
if env.events() != 1 || action != "media.variant_generate_all" || event["outcome"] != "success" || params["generated"] != 5.0 || params["failed"] != 0.0 || causes == nil || len(causes) != 0 {
t.Errorf("event = %v", event)
}
assertImageTarget(t, event, "1", "north-wall.jpg")
})
t.Run("A partial failure is a success that names its causes", func(t *testing.T) {
env := newVariantEnv(t)
env.seedFiveEnabled()
env.failHero(domain.ErrSourceTooLarge)
env.send(http.MethodPost, "/v1/media/1/variants", "", variantOwnerId)
_, event := env.lastEvent()
params := event["params"].(map[string]any)
causes := params["causes"].([]any)
if env.events() != 1 || event["outcome"] != "success" || params["generated"] != 4.0 || params["failed"] != 1.0 || len(causes) != 1 || causes[0] != "too_large" {
t.Errorf("event = %v", event)
}
})
t.Run("Every variant failing is a failed event with a plain reason", func(t *testing.T) {
env := newVariantEnv(t)
env.seedFiveEnabled()
env.renderer.failFor = func(domain.VariantSettings) error { return domain.ErrSourceTooLarge }
recorder := env.send(http.MethodPost, "/v1/media/1/variants", "", variantOwnerId)
body := decodeBody(t, recorder)
_, event := env.lastEvent()
if recorder.Code != 200 || body["generated"] != 0.0 || body["failed"] != 5.0 || env.events() != 1 || event["action"] != "media.variant_generate_all" || event["outcome"] != "failed" || event["failure_reason"] != tooLargeReason {
t.Errorf("response = %d %q, event = %v", recorder.Code, recorder.Body.String(), event)
}
})
t.Run("The failure reasons of the other causes are plain sentences", func(t *testing.T) {
causes := map[string]error{
"Variants aren't generated for this format.": domain.ErrVariantSourceUnsupported,
"The source image can't be read, so this variant can't be generated.": domain.ErrVariantSourceUnreadable,
genericReason: errors.New("boom"),
}
for reason, cause := range causes {
env := newVariantEnv(t)
env.seedFiveEnabled()
env.renderer.failFor = func(domain.VariantSettings) error { return cause }
env.send(http.MethodPost, "/v1/media/1/variants", "", variantOwnerId)
if _, event := env.lastEvent(); event["failure_reason"] != reason {
t.Errorf("reason for %v = %v, want %q", cause, event["failure_reason"], reason)
}
}
})
noEvent := []struct {
name string
prepare func(env *variantEnv)
method string
path string
}{
{"all five are already generated", func(env *variantEnv) {
env.seedFiveEnabled()
env.send(http.MethodPost, "/v1/media/1/variants", "", variantOwnerId)
}, http.MethodPost, "/v1/media/1/variants"},
{"no variant is enabled", func(env *variantEnv) { env.seedPerImage(1, "north-wall.jpg", "a1b2.png", "image/png", 10, 10, nil) }, http.MethodPost, "/v1/media/1/variants"},
{"an SVG", func(env *variantEnv) {
env.seedFiveEnabled()
env.seedPerImage(1, "logo.svg", "e5f6.svg", "image/svg+xml", 0, 0, nil)
}, http.MethodPost, "/v1/media/1/variants"},
{"a single generation", func(env *variantEnv) { env.seedFiveEnabled() }, http.MethodPost, "/v1/media/1/variants/thumb"},
{"an unknown image", func(env *variantEnv) { env.seedFiveEnabled() }, http.MethodPost, "/v1/media/9999/variants"},
{"a listing", func(env *variantEnv) { env.seedFiveEnabled() }, http.MethodGet, "/v1/media/1/variants"},
}
for _, row := range noEvent {
t.Run("Nothing was done, nothing is recorded: "+row.name, func(t *testing.T) {
env := newVariantEnv(t)
row.prepare(env)
before := env.events()
env.send(row.method, row.path, "", variantOwnerId)
if env.events() != before {
t.Errorf("events = %d, want %d", env.events(), before)
}
})
}
t.Run("Nothing was done, nothing is recorded: a public generation", func(t *testing.T) {
env := newVariantEnv(t)
env.seedFiveEnabled()
recorder := env.get("/static/uploadedImages/a1b2.png?v=thumb", nil)
if recorder.Code != 200 || env.events() != 0 {
t.Errorf("status = %d, events = %d", recorder.Code, env.events())
}
})
t.Run("Purging the files of one variant of an image is recorded", func(t *testing.T) {
env := newVariantEnv(t)
env.seedFiveEnabled()
env.seedCurrentFile("thumb", "a1b2.png", 18432)
env.send(http.MethodDelete, "/v1/media/1/variants/thumb", "", variantOwnerId)
action, event := env.lastEvent()
params := event["params"].(map[string]any)
if env.events() != 1 || action != "media.variant_purge_image" || event["outcome"] != "success" || params["purged_files"] != 1.0 || params["purged_bytes"] != 18432.0 {
t.Errorf("event = %v", event)
}
assertImageTarget(t, event, "1", "north-wall.jpg")
})
purgeNoEvent := []struct{ name, path string }{
{"nothing is generated", "/v1/media/1/variants/thumb"},
{"the variant is unknown", "/v1/media/1/variants/missing"},
{"the image is unknown", "/v1/media/9999/variants/thumb"},
}
for _, row := range purgeNoEvent {
t.Run("A purge that removes nothing, or finds nothing, is not recorded: "+row.name, func(t *testing.T) {
env := newVariantEnv(t)
env.seedFiveEnabled()
env.send(http.MethodDelete, row.path, "", variantOwnerId)
if env.events() != 0 {
t.Errorf("events = %d", env.events())
}
})
}
t.Run("A purge that fails on storage is recorded as failed", func(t *testing.T) {
env := newVariantEnv(t)
env.seedFiveEnabled()
env.seedCurrentFile("thumb", "a1b2.png", 18432)
env.storage.failDelete["cached-thumb-a1b2.png"] = errVariantStorageFailure
recorder := env.send(http.MethodDelete, "/v1/media/1/variants/thumb", "", variantOwnerId)
action, event := env.lastEvent()
if recorder.Code != 500 || env.events() != 1 || action != "media.variant_purge_image" || event["outcome"] != "failed" || event["failure_reason"] != genericReason {
t.Errorf("status = %d, event = %v", recorder.Code, event)
}
})
t.Run("A failing audit write never changes the response", func(t *testing.T) {
env := newVariantEnv(t)
env.seedFiveEnabled()
env.audit.probe.appendErr = errDiskFull
recorder := env.send(http.MethodPost, "/v1/media/1/variants", "", variantOwnerId)
if recorder.Code != 200 || decodeBody(t, recorder)["generated"] != 5.0 || env.files.rowCount() != 5 {
t.Errorf("response = %d %q", recorder.Code, recorder.Body.String())
}
})
t.Run("The new events read back through the existing activity API", func(t *testing.T) {
env := newVariantEnv(t)
env.seedFiveEnabled()
env.send(http.MethodPost, "/v1/media/1/variants", "", variantOwnerId)
env.send(http.MethodDelete, "/v1/media/1/variants/thumb", "", variantOwnerId)
recorder := env.send(http.MethodGet, "/v1/activity?category=media", "", variantOwnerId)
items := itemsOf(t, recorder)
actions := []any{}
for _, item := range items {
actions = append(actions, item["action"])
if item["target"].(map[string]any)["type"] != "image" || item["params"] == nil {
t.Errorf("item = %v", item)
}
}
if recorder.Code != 200 || len(items) != 2 || actions[0] != "media.variant_purge_image" || actions[1] != "media.variant_generate_all" {
t.Errorf("response = %d %q", recorder.Code, recorder.Body.String())
}
})
t.Run("An upload that generates files records only the upload", func(t *testing.T) {
env := newVariantEnv(t)
env.seedUploadVariants()
recorder := env.upload(uploadedFilename, "image/png", solidPNG(t, 1000, 800))
if action, _ := env.lastEvent(); recorder.Code != 201 || env.events() != 1 || action != "media.upload" {
t.Errorf("status = %d, events = %d", recorder.Code, env.events())
}
})
}