Files
RenewCMS/test/media_variants_list_http_test.go
T

265 lines
9.1 KiB
Go

package test
import (
"RenewCMS/api/pkg/apperr"
"RenewCMS/internal/domain/article"
domain "RenewCMS/internal/domain/image"
"net/http"
"slices"
"strings"
"testing"
)
const libraryCounts = `{"all":5,"unused":3,"duplicates":1}`
func (e *variantEnv) saveRow(variantName string, storedName string, size int64) {
e.files.Save(domain.VariantFile{VariantName: variantName, SourceName: storedName, CachedName: "cached-" + variantName + "-" + storedName, ContentType: "image/webp", Size: size, Fingerprint: "f"})
e.storage.seed("cached-"+variantName+"-"+storedName, []byte("generated"))
}
func (e *variantEnv) seedLibrary() {
library := []struct {
id uint32
filename string
storedName string
contentType string
size int64
sha string
}{
{1, "north-wall.jpg", "a1b2.jpg", "image/jpeg", 4089446, "hash-north"},
{2, "floorboards.jpg", "f2f2.jpg", "image/jpeg", 2516582, "hash-floor"},
{3, "floorboards-copy.jpg", "f3f3.jpg", "image/jpeg", 2516582, "hash-floor"},
{4, "window-frame.jpg", "w4w4.jpg", "image/jpeg", 812345, "hash-window"},
{5, "logo.svg", "e5f6.svg", "image/svg+xml", 1204, "hash-logo"},
}
for _, entry := range library {
img := e.seedPerImage(entry.id, entry.filename, entry.storedName, entry.contentType, 100, 100, nil)
img.Size, img.SHA256 = entry.size, entry.sha
e.images.images[entry.id] = img
}
e.articles.articles[1] = article.Article{ID: 1, Title: "Walls", Body: "see /static/uploadedImages/a1b2.jpg and /static/uploadedImages/f2f2.jpg", IsOnline: true}
for variantName, size := range map[string]int64{"thumb": 18432, "card": 65536, "hero": 98304, "square": 83968} {
e.saveRow(variantName, "a1b2.jpg", size)
}
e.saveRow("thumb", "f2f2.jpg", 41000)
for _, variantName := range []string{"old-a", "old-b", "old-c"} {
e.saveRow(variantName, "w4w4.jpg", 1000000)
}
}
func (e *variantEnv) listIds(query string) ([]float64, map[string]any) {
e.t.Helper()
recorder := e.send(http.MethodGet, "/v1/media/"+query, "", variantOwnerId)
if recorder.Code != 200 {
e.t.Fatalf("list %q = %d %q", query, recorder.Code, recorder.Body.String())
}
body := decodeBody(e.t, recorder)
ids := []float64{}
for _, item := range body["items"].([]any) {
ids = append(ids, item.(map[string]any)["id"].(float64))
}
return ids, body
}
func itemWithId(t *testing.T, body map[string]any, id float64) map[string]any {
t.Helper()
for _, item := range body["items"].([]any) {
if item.(map[string]any)["id"] == id {
return item.(map[string]any)
}
}
t.Fatalf("no item %v", id)
return nil
}
func TestMediaListCarriesVariantFields(t *testing.T) {
t.Run("Every item carries variant_count and variant_size", func(t *testing.T) {
env := newVariantEnv(t)
env.seedLibrary()
ids, body := env.listIds("")
if !slices.Equal(ids, []float64{5, 4, 3, 2, 1}) {
t.Fatalf("order = %v", ids)
}
expected := map[float64][2]float64{1: {4, 266240}, 2: {1, 41000}, 3: {0, 0}, 4: {3, 3000000}, 5: {0, 0}}
for id, want := range expected {
item := itemWithId(t, body, id)
if item["variant_count"] != want[0] || item["variant_size"] != want[1] {
t.Errorf("item %v = %v", id, item)
}
}
if itemWithId(t, body, 2)["duplicate"] != true || itemWithId(t, body, 1)["used_in"] != 1.0 {
t.Error("duplicate or used_in changed")
}
})
t.Run("The list reads the totals of all sources in one call", func(t *testing.T) {
env := newVariantEnv(t)
env.seedLibrary()
env.listIds("")
if env.files.totalsBySourceCalls != 1 {
t.Errorf("calls = %d", env.files.totalsBySourceCalls)
}
})
t.Run("A rename answers the same fields", func(t *testing.T) {
env := newVariantEnv(t)
env.seedLibrary()
recorder := env.send(http.MethodPatch, "/v1/media/1", `{"filename": "north-wall-01"}`, variantOwnerId)
body := decodeBody(t, recorder)
if recorder.Code != 200 || body["variant_count"] != 4.0 || body["variant_size"] != 266240.0 {
t.Errorf("response = %d %q", recorder.Code, recorder.Body.String())
}
})
}
func TestMediaListVariantsFilterAndSort(t *testing.T) {
filters := map[string][]float64{"with": {1, 2, 4}, "without": {3, 5}}
for variants, want := range filters {
t.Run("variants narrows the list and the counts stay library-wide: "+variants, func(t *testing.T) {
env := newVariantEnv(t)
env.seedLibrary()
ids, body := env.listIds("?variants=" + variants)
slices.Sort(ids)
counts := body["counts"].(map[string]any)
if !slices.Equal(ids, want) || counts["all"] != 5.0 || counts["unused"] != 3.0 || counts["duplicates"] != 1.0 {
t.Errorf("ids = %v, counts = %v", ids, counts)
}
})
}
t.Run("variants, filter, q and sort combine", func(t *testing.T) {
env := newVariantEnv(t)
env.seedLibrary()
unused, _ := env.listIds("?variants=with&filter=unused")
searched, _ := env.listIds("?variants=with&q=floorboards")
if !slices.Equal(unused, []float64{4}) || !slices.Equal(searched, []float64{2}) {
t.Errorf("unused = %v, searched = %v", unused, searched)
}
})
t.Run("sort=total_size orders by original plus variants, largest first, then by id", func(t *testing.T) {
env := newVariantEnv(t)
env.seedLibrary()
ids, _ := env.listIds("?sort=total_size")
if !slices.Equal(ids, []float64{1, 4, 2, 3, 5}) {
t.Errorf("order = %v", ids)
}
})
t.Run("sort=largest is unchanged and sorts by the original alone", func(t *testing.T) {
env := newVariantEnv(t)
env.seedLibrary()
ids, _ := env.listIds("?sort=largest")
if !slices.Equal(ids, []float64{1, 2, 3, 4, 5}) {
t.Errorf("order = %v", ids)
}
})
t.Run("Equal totals fall back to the lowest id first", func(t *testing.T) {
env := newVariantEnv(t)
env.seedLibrary()
third := env.images.images[3]
third.Size = 2557582
env.images.images[3] = third
ids, _ := env.listIds("?sort=total_size")
if !slices.Equal(ids, []float64{1, 4, 2, 3, 5}) {
t.Errorf("order = %v", ids)
}
})
t.Run("variants=with and sort=total_size answer the ordered page with its total", func(t *testing.T) {
env := newVariantEnv(t)
env.seedLibrary()
ids, body := env.listIds("?variants=with&sort=total_size&size=5&page=1")
beyond, _ := env.listIds("?variants=with&sort=total_size&size=5&page=2")
if !slices.Equal(ids, []float64{1, 4, 2}) || body["total"] != float64(3) || len(beyond) != 0 {
t.Errorf("ids = %v, total = %v, beyond = %v", ids, body["total"], beyond)
}
})
t.Run("variants=with on a library with an empty cache answers an empty list", func(t *testing.T) {
env := newVariantEnv(t)
env.seedLibrary()
for _, row := range env.files.rows {
_ = env.files.Delete(row.ID)
}
recorder := env.send(http.MethodGet, "/v1/media/?variants=with", "", variantOwnerId)
if recorder.Code != 200 || !strings.Contains(recorder.Body.String(), `"items":[]`) || !strings.Contains(recorder.Body.String(), `"counts":`+libraryCounts) {
t.Errorf("response = %d %q", recorder.Code, recorder.Body.String())
}
})
for _, query := range []string{"variants=bogus", "variants=all", "variants=With", "variants=", "sort=Total_size", "sort=total"} {
t.Run("A malformed query answers 400: "+query, func(t *testing.T) {
env := newVariantEnv(t)
env.seedLibrary()
recorder := env.send(http.MethodGet, "/v1/media/?"+query, "", variantOwnerId)
assertStatusAndBody(t, recorder, 400, apperr.MediaQueryErrorMessage)
})
}
}
func TestMediaLibraryVariantConsistency(t *testing.T) {
t.Run("The number shown by a delete is the number a delete removes", func(t *testing.T) {
env := newVariantEnv(t)
env.seedLibrary()
env.storage.seed("a1b2.jpg", []byte("original"))
recorder := env.send(http.MethodDelete, "/v1/media/1", "", variantOwnerId)
deleted := env.storage.deletedNames()
slices.Sort(deleted)
expected := []string{"a1b2.jpg", "cached-card-a1b2.jpg", "cached-hero-a1b2.jpg", "cached-square-a1b2.jpg", "cached-thumb-a1b2.jpg"}
if recorder.Code != 204 || !slices.Equal(deleted, expected) || len(env.files.rowsOf("hero")) != 0 || env.files.rowCount() != 4 {
t.Errorf("status = %d, deleted = %v, rows = %d", recorder.Code, deleted, env.files.rowCount())
}
})
t.Run("An article that uses a variant url still counts as using the image", func(t *testing.T) {
env := newVariantEnv(t)
env.seedLibrary()
env.articles.articles[1] = article.Article{ID: 1, Title: "Walls", Body: "![x](/static/uploadedImages/a1b2.jpg?v=hero)", IsOnline: true}
usage := env.send(http.MethodGet, "/v1/media/1/usage", "", variantOwnerId)
_, body := env.listIds("")
if usage.Code != 200 || !strings.Contains(usage.Body.String(), `"id":1`) || itemWithId(t, body, 1)["used_in"] != 1.0 {
t.Errorf("usage = %d %q", usage.Code, usage.Body.String())
}
})
t.Run("Merging keeps the variant parameter of the switched url", func(t *testing.T) {
env := newVariantEnv(t)
env.seedLibrary()
env.articles.articles[1] = article.Article{ID: 1, Title: "Floors", Body: "![x](/static/uploadedImages/f3f3.jpg?v=hero)", IsOnline: true}
recorder := env.send(http.MethodPost, "/v1/media/merge", `{"keep_id": 2, "merge_ids": [3]}`, variantOwnerId)
if recorder.Code != 200 || !strings.Contains(env.articles.articles[1].Body, "/static/uploadedImages/f2f2.jpg?v=hero") {
t.Errorf("status = %d, body = %q", recorder.Code, env.articles.articles[1].Body)
}
})
}