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()) } }) }