Files
RenewCMS/test/image_variants_audit_test.go
T
2026-10-08 02:33:32 +02:00

279 lines
10 KiB
Go

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