package test import ( "RenewCMS/internal/domain/article" "RenewCMS/internal/domain/audit" domain "RenewCMS/internal/domain/image" "encoding/json" "errors" "net/http" "slices" "testing" ) func eventsOf(env *variantEnv, action string) []audit.Event { matching := []audit.Event{} for _, event := range env.audit.all(env.t) { if event.Action == action { matching = append(matching, event) } } return matching } func onlyEventOf(t *testing.T, env *variantEnv, action string) audit.Event { t.Helper() matching := eventsOf(env, action) if len(matching) != 1 || env.events() != 1 { t.Fatalf("%d %s events among %d", len(matching), action, env.events()) } return matching[0] } func marshalled(t *testing.T, value any) string { t.Helper() encoded, err := json.Marshal(value) if err != nil { t.Fatal(err) } return string(encoded) } func assertVariantTarget(t *testing.T, event audit.Event, name string) { t.Helper() if got := marshalled(t, event.Target); got != `{"type":"image_variant","id":"`+name+`","name":"`+name+`"}` { t.Errorf("target = %s", got) } } func ownerHeroEnv(t *testing.T) *variantEnv { t.Helper() env := newVariantEnv(t) seedVariant(env.variants, "hero", 1200, 630, domain.FitCover, domain.FormatWebP, 82) seedVariantFiles(env.files, env.storage, "hero", "src", 244, 344000) return env } func TestImageVariantsActivityLog(t *testing.T) { t.Run("Creating a variant is recorded", func(t *testing.T) { env := newVariantEnv(t) env.send(http.MethodPost, "/v1/image-variants", `{"name": "hero", "width": 1200, "height": 630, "fit": "cover", "format": "webp", "quality": 82}`, variantOwnerId) event := onlyEventOf(t, env, "media.variant_create") assertVariantTarget(t, event, "hero") if event.Outcome != audit.OutcomeSuccess || event.Category != audit.CategoryMedia || event.Source != audit.SourceWeb || event.Actor.Name != "Marta Alves" || *event.Actor.ID != variantOwnerId { t.Errorf("event = %+v", event) } if got := marshalled(t, event.Params); got != `{"fit":"cover","format":"webp","height":630,"quality":82,"width":1200}` { t.Errorf("params = %s", got) } }) t.Run("Creating a width-only png variant records no quality and no height", func(t *testing.T) { env := newVariantEnv(t) env.send(http.MethodPost, "/v1/image-variants", `{"name": "logo", "width": 100, "format": "png"}`, variantOwnerId) event := onlyEventOf(t, env, "media.variant_create") if got := marshalled(t, event.Params); got != `{"fit":"cover","format":"png","width":100}` { t.Errorf("params = %s", got) } }) t.Run("Updating a variant is recorded with the diff and the purge count", func(t *testing.T) { env := ownerHeroEnv(t) env.send(http.MethodPut, "/v1/image-variants/hero", `{"width": 1200, "height": 630, "fit": "cover", "format": "webp", "quality": 70}`, variantOwnerId) event := onlyEventOf(t, env, "media.variant_update") assertVariantTarget(t, event, "hero") if got := marshalled(t, event.Changes); got != `[{"field":"quality","kind":"text","before":"82","after":"70"}]` { t.Errorf("changes = %s", got) } if got := marshalled(t, event.Params); got != `{"purged_files":244}` { t.Errorf("params = %s", got) } }) t.Run("A removed dimension is recorded as auto", func(t *testing.T) { env := ownerHeroEnv(t) env.send(http.MethodPut, "/v1/image-variants/hero", `{"width": 1200, "fit": "cover", "format": "webp", "quality": 82}`, variantOwnerId) event := onlyEventOf(t, env, "media.variant_update") if got := marshalled(t, event.Changes); got != `[{"field":"height","kind":"text","before":"630","after":"auto"}]` { t.Errorf("changes = %s", got) } }) t.Run("A switch-only update does not claim a purge", func(t *testing.T) { env := ownerHeroEnv(t) env.send(http.MethodPut, "/v1/image-variants/hero", `{"width": 1200, "height": 630, "fit": "cover", "format": "webp", "quality": 82, "generate_on_upload": true}`, variantOwnerId) if env.events() != 0 { t.Errorf("events = %d", env.events()) } }) t.Run("Disabling through a PUT is recorded as a disable and not as an update", func(t *testing.T) { env := ownerHeroEnv(t) env.send(http.MethodPut, "/v1/image-variants/hero", `{"width": 1200, "height": 630, "fit": "cover", "format": "webp", "quality": 82, "enabled": false}`, variantOwnerId) assertVariantTarget(t, onlyEventOf(t, env, "media.variant_disable"), "hero") }) t.Run("Deleting a variant is recorded with the removed files", func(t *testing.T) { env := ownerHeroEnv(t) env.send(http.MethodDelete, "/v1/image-variants/hero", "", variantOwnerId) event := onlyEventOf(t, env, "media.variant_delete") assertVariantTarget(t, event, "hero") if got := marshalled(t, event.Params); got != `{"purged_bytes":83936000,"purged_files":244}` { t.Errorf("params = %s", got) } }) for _, row := range []struct { before, after bool action string }{{false, true, "media.variant_enable"}, {true, false, "media.variant_disable"}} { t.Run("Enabling and disabling are recorded: "+row.action, func(t *testing.T) { env := ownerHeroEnv(t) hero := env.variants.stored("hero") hero.Enabled = row.before env.variants.variants["hero"] = hero payload := `{"enabled": false}` if row.after { payload = `{"enabled": true}` } env.send(http.MethodPatch, "/v1/image-variants/hero", payload, variantOwnerId) assertVariantTarget(t, onlyEventOf(t, env, row.action), "hero") }) } t.Run("A PATCH to the current value records nothing", func(t *testing.T) { env := ownerHeroEnv(t) recorder := env.send(http.MethodPatch, "/v1/image-variants/hero", `{"enabled": true}`, variantOwnerId) if recorder.Code != 200 || env.events() != 0 { t.Errorf("status = %d, events = %d", recorder.Code, env.events()) } }) t.Run("A refused create is recorded as failed", func(t *testing.T) { env := newVariantEnv(t) env.seedThumb() env.send(http.MethodPost, "/v1/image-variants", `{"name": "thumb", "width": 10}`, variantOwnerId) event := onlyEventOf(t, env, "media.variant_create") assertVariantTarget(t, event, "thumb") if event.Outcome != audit.OutcomeFailed || event.FailureReason == nil || *event.FailureReason != "A variant with that name already exists." { t.Errorf("event = %+v", event) } }) nothing := []struct{ name, method, path, body string }{ {"validation failure", http.MethodPost, "/v1/image-variants", `{"name": "hero", "width": 9000}`}, {"unknown PUT", http.MethodPut, "/v1/image-variants/missing", `{"width": 100}`}, {"unknown DELETE", http.MethodDelete, "/v1/image-variants/missing", ""}, {"unknown PATCH", http.MethodPatch, "/v1/image-variants/missing", `{"enabled": true}`}, {"unknown purge", http.MethodPost, "/v1/image-variants/missing/purge", ""}, {"list", http.MethodGet, "/v1/image-variants", ""}, } for _, row := range nothing { t.Run("Requests that fail validation or find nothing record nothing: "+row.name, func(t *testing.T) { env := ownerHeroEnv(t) env.send(row.method, row.path, row.body, variantOwnerId) if env.events() != 0 { t.Errorf("events = %d", env.events()) } }) } t.Run("A public GET that generates a file records nothing", func(t *testing.T) { env := servingEnv(t) env.get(thumbURL, nil) if env.events() != 0 { t.Errorf("events = %d", env.events()) } }) t.Run("Purging a variant's cache is recorded", func(t *testing.T) { env := ownerHeroEnv(t) env.send(http.MethodPost, "/v1/image-variants/hero/purge", "", variantOwnerId) event := onlyEventOf(t, env, "media.variant_purge") assertVariantTarget(t, event, "hero") if got := marshalled(t, event.Params); event.Outcome != audit.OutcomeSuccess || got != `{"purged_bytes":83936000,"purged_files":244}` { t.Errorf("params = %s", got) } }) t.Run("Purging an empty cache records nothing", func(t *testing.T) { env := newVariantEnv(t) seedVariant(env.variants, "hero", 100, 0, domain.FitCover, domain.FormatJPEG, 80) recorder := env.send(http.MethodPost, "/v1/image-variants/hero/purge", "", variantOwnerId) if recorder.Code != 200 || env.events() != 0 { t.Errorf("status = %d, events = %d", recorder.Code, env.events()) } }) t.Run("A purge that fails on storage is recorded as failed", func(t *testing.T) { env := newVariantEnv(t) seedVariant(env.variants, "hero", 100, 0, domain.FitCover, domain.FormatJPEG, 80) rows := seedVariantFiles(env.files, env.storage, "hero", "src", 3, 10) env.storage.failDelete[rows[1].CachedName] = errVariantStorageFailure recorder := env.send(http.MethodPost, "/v1/image-variants/hero/purge", "", variantOwnerId) event := onlyEventOf(t, env, "media.variant_purge") if recorder.Code != 500 || event.Outcome != audit.OutcomeFailed || event.FailureReason == nil || *event.FailureReason != "Something went wrong on our side. Please try again." { 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.audit.probe.appendErr = errors.New("audit store down") recorder := env.send(http.MethodPost, "/v1/image-variants", `{"name": "hero", "width": 100}`, variantOwnerId) if recorder.Code != 201 || env.variants.stored("hero").Name != "hero" { t.Errorf("status = %d", recorder.Code) } }) t.Run("The existing allowlist scenario is updated", func(t *testing.T) { allowed := []string{"title", "online", "body", "filename", "display_name", "width", "height", "fit", "format", "quality"} emitted := []audit.Change{} emitted = append(emitted, audit.ArticleChanges(article.Article{Title: "a", Body: "x"}, article.Article{Title: "b", Body: "y", IsOnline: true})...) before := domain.VariantSettings{Width: intPointer(1), Height: intPointer(2), Fit: domain.FitCover, Format: domain.FormatJPEG, Quality: intPointer(80)} after := domain.VariantSettings{Width: intPointer(3), Fit: domain.FitInside, Format: domain.FormatPNG} emitted = append(emitted, audit.VariantChanges(before, after)...) for _, change := range emitted { if !slices.Contains(allowed, change.Field) || change.Kind != audit.ChangeText && change.Kind != audit.ChangeBody { t.Errorf("unexpected change %+v", change) } } if len(emitted) != 8 { t.Errorf("emitted %d changes", len(emitted)) } }) }