350 lines
12 KiB
Go
350 lines
12 KiB
Go
package test
|
|
|
|
import (
|
|
"RenewCMS/api/pkg/apperr"
|
|
domainImage "RenewCMS/internal/domain/image"
|
|
"RenewCMS/internal/infrastructure/useCases"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func seedBulkImages(env *mediaEnv, count int) {
|
|
for id := uint32(1); id <= uint32(count); id++ {
|
|
env.seedImage(id, fmt.Sprintf("image-%02d.jpg", id), int64(id)*10, "image/jpeg", 3, 2, hashA, 1)
|
|
}
|
|
}
|
|
|
|
func TestMediaPaginationEnvelope(t *testing.T) {
|
|
t.Run("A bare request answers the envelope with the defaults", func(t *testing.T) {
|
|
env := newMediaEnv(t)
|
|
seedBulkImages(env, 30)
|
|
|
|
got, raw := env.list(t, "")
|
|
|
|
if got.Page != 1 || got.Size != 25 || got.Total != 30 || len(got.Items) != 25 {
|
|
t.Errorf("unexpected envelope %d %d %d %d", got.Page, got.Size, got.Total, len(got.Items))
|
|
}
|
|
if got.Counts["all"] != 30 {
|
|
t.Errorf("unexpected counts %v", got.Counts)
|
|
}
|
|
if strings.Contains(raw, "next_cursor") {
|
|
t.Errorf("unexpected next_cursor in %s", raw)
|
|
}
|
|
})
|
|
|
|
t.Run("Defaults when only one parameter is given", func(t *testing.T) {
|
|
examples := []struct {
|
|
query string
|
|
page int
|
|
size int
|
|
count int
|
|
}{
|
|
{"?page=2", 2, 25, 25},
|
|
{"?page=3", 3, 25, 10},
|
|
{"?size=5", 1, 5, 5},
|
|
{"?size=50", 1, 50, 50},
|
|
{"?size=100", 1, 100, 60},
|
|
}
|
|
for _, example := range examples {
|
|
t.Run(example.query, func(t *testing.T) {
|
|
env := newMediaEnv(t)
|
|
seedBulkImages(env, 60)
|
|
|
|
got, _ := env.list(t, example.query)
|
|
|
|
if got.Page != example.page || got.Size != example.size || got.Total != 60 || len(got.Items) != example.count {
|
|
t.Errorf("unexpected envelope %d %d %d %d", got.Page, got.Size, got.Total, len(got.Items))
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("Pages partition the list with no overlap", func(t *testing.T) {
|
|
env := newMediaEnv(t)
|
|
seedBulkImages(env, 12)
|
|
seen := map[uint32]bool{}
|
|
|
|
for page, want := range map[int]int{1: 5, 2: 5, 3: 2} {
|
|
got, _ := env.list(t, fmt.Sprintf("?page=%d&size=5", page))
|
|
if len(got.Items) != want || got.Total != 12 {
|
|
t.Errorf("page %d holds %d of %d", page, len(got.Items), got.Total)
|
|
}
|
|
for _, id := range idsOf(got.Items) {
|
|
if seen[id] {
|
|
t.Errorf("id %d repeats", id)
|
|
}
|
|
seen[id] = true
|
|
}
|
|
}
|
|
if len(seen) != 12 {
|
|
t.Errorf("expected 12 distinct ids, got %d", len(seen))
|
|
}
|
|
})
|
|
|
|
t.Run("Newest first with the id tie-break", func(t *testing.T) {
|
|
env := newMediaEnv(t)
|
|
for id := uint32(1); id <= 3; id++ {
|
|
env.seedImage(id, "a.jpg", 10, "image/jpeg", 3, 2, hashA, 1)
|
|
}
|
|
sameDay := time.Date(2026, time.September, 3, 0, 0, 0, 0, time.UTC)
|
|
for _, id := range []uint32{2, 3} {
|
|
img := env.images.images[id]
|
|
img.CreatedAt = sameDay
|
|
env.images.images[id] = img
|
|
}
|
|
first := env.images.images[1]
|
|
first.CreatedAt = sameDay.AddDate(0, 0, -2)
|
|
env.images.images[1] = first
|
|
|
|
got, _ := env.list(t, "?size=5")
|
|
|
|
expectIds(t, idsOf(got.Items), []uint32{3, 2, 1})
|
|
})
|
|
|
|
t.Run("Sort orders the whole library before paging", func(t *testing.T) {
|
|
names := []string{"mango", "Apple", "cherry", "banana", "Zucchini", "date", "fig", "Grape", "kiwi", "lemon", "olive", "pear"}
|
|
sizes := []int64{50, 120, 10, 90, 30, 110, 70, 20, 100, 60, 40, 80}
|
|
examples := map[string][]uint32{
|
|
"newest": {12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1},
|
|
"oldest": {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12},
|
|
"largest": {2, 6, 9, 4, 12, 7, 10, 1, 11, 5, 8, 3},
|
|
"name": {2, 4, 3, 6, 7, 8, 9, 10, 1, 11, 12, 5},
|
|
"total_size": {3, 8, 2, 6, 9, 4, 12, 7, 10, 1, 11, 5},
|
|
}
|
|
for sortName, want := range examples {
|
|
t.Run(sortName, func(t *testing.T) {
|
|
env := newMediaEnv(t)
|
|
for index, name := range names {
|
|
env.seedImage(uint32(index+1), name+".jpg", sizes[index], "image/jpeg", 3, 2, hashA, 1)
|
|
}
|
|
for id, bytes := range map[uint32]int64{3: 500, 8: 200} {
|
|
_, _ = env.variants.Save(domainImage.VariantFile{VariantName: "hero", SourceName: domainImage.StoredName(env.images.images[id].Path), CachedName: fmt.Sprintf("c%d", id), ContentType: "image/jpeg", Size: bytes, Fingerprint: "f"})
|
|
}
|
|
var all []uint32
|
|
for page := 1; page <= 3; page++ {
|
|
got, _ := env.list(t, fmt.Sprintf("?sort=%s&size=5&page=%d", sortName, page))
|
|
all = append(all, idsOf(got.Items)...)
|
|
}
|
|
expectIds(t, all, want)
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("Filter and search narrow total and items but not the counts", func(t *testing.T) {
|
|
env := newMediaEnv(t)
|
|
seedBulkImages(env, 40)
|
|
for id := uint32(1); id <= 34; id++ {
|
|
env.seedArticle(id, "Article", true, env.urlOf(id))
|
|
}
|
|
|
|
unused, _ := env.list(t, "?filter=unused&size=5")
|
|
search, _ := env.list(t, "?q=image-1&size=5&page=2")
|
|
|
|
if unused.Total != 6 || len(unused.Items) != 5 || unused.Counts["all"] != 40 || unused.Counts["unused"] != 6 {
|
|
t.Errorf("unexpected unused page %d %d %v", unused.Total, len(unused.Items), unused.Counts)
|
|
}
|
|
if search.Total != 10 || len(search.Items) != 5 || search.Counts["all"] != 40 {
|
|
t.Errorf("unexpected search page %d %d %v", search.Total, len(search.Items), search.Counts)
|
|
}
|
|
})
|
|
|
|
t.Run("Hash lookup returns every copy in one page", func(t *testing.T) {
|
|
env := newMediaEnv(t)
|
|
seedListingLibrary(env)
|
|
env.seedImage(6, "floorboards-2.jpg", 10, "image/jpeg", 3, 2, hashB, 1)
|
|
|
|
explicit, _ := env.list(t, "?hash="+hashB+"&page=1&size=100")
|
|
bare, _ := env.list(t, "?hash="+hashB)
|
|
|
|
if explicit.Total != 3 || len(explicit.Items) != 3 {
|
|
t.Errorf("unexpected explicit lookup %d %d", explicit.Total, len(explicit.Items))
|
|
}
|
|
if bare.Page != 1 || bare.Size != 25 || bare.Total != 3 || len(bare.Items) != 3 {
|
|
t.Errorf("unexpected bare lookup %d %d %d %d", bare.Page, bare.Size, bare.Total, len(bare.Items))
|
|
}
|
|
})
|
|
|
|
t.Run("A page beyond the last is empty with the real total", func(t *testing.T) {
|
|
env := newMediaEnv(t)
|
|
seedBulkImages(env, 12)
|
|
|
|
got, raw := env.list(t, "?page=99&size=5")
|
|
|
|
if len(got.Items) != 0 || got.Total != 12 || got.Page != 99 || got.Size != 5 || got.Counts["all"] != 12 {
|
|
t.Errorf("unexpected envelope %+v", got)
|
|
}
|
|
if !strings.Contains(raw, `"items":[]`) {
|
|
t.Errorf("expected an empty array in %s", raw)
|
|
}
|
|
})
|
|
|
|
t.Run("An empty library", func(t *testing.T) {
|
|
env := newMediaEnv(t)
|
|
|
|
got, raw := env.list(t, "")
|
|
|
|
if len(got.Items) != 0 || got.Total != 0 || !reflect.DeepEqual(got.Counts, map[string]int{"all": 0, "unused": 0, "duplicates": 0}) {
|
|
t.Errorf("unexpected envelope %+v", got)
|
|
}
|
|
if !strings.Contains(raw, `"items":[]`) {
|
|
t.Errorf("expected an empty array in %s", raw)
|
|
}
|
|
})
|
|
|
|
t.Run("counts ignores every filter and the page", func(t *testing.T) {
|
|
env := newMediaEnv(t)
|
|
seedBulkImages(env, 30)
|
|
|
|
got, _ := env.list(t, "?q=zzzz&page=3&size=5")
|
|
|
|
if len(got.Items) != 0 || got.Total != 0 || got.Counts["all"] != 30 {
|
|
t.Errorf("unexpected envelope %+v", got)
|
|
}
|
|
})
|
|
|
|
t.Run("cursor and limit are ignored", func(t *testing.T) {
|
|
for _, query := range []string{"cursor=abc", "limit=5", "limit=0", "limit=abc", "cursor=b2Zmc2V0OjU"} {
|
|
t.Run(query, func(t *testing.T) {
|
|
env := newMediaEnv(t)
|
|
seedBulkImages(env, 30)
|
|
|
|
got, _ := env.list(t, "?"+query)
|
|
bare, _ := env.list(t, "")
|
|
|
|
if got.Page != 1 || got.Size != 25 || len(got.Items) != 25 {
|
|
t.Errorf("unexpected envelope %d %d %d", got.Page, got.Size, len(got.Items))
|
|
}
|
|
expectIds(t, idsOf(got.Items), idsOf(bare.Items))
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("Invalid parameters answer 400", func(t *testing.T) {
|
|
queries := []string{"page=0", "page=-1", "page=abc", "page=", "page=1.5", "size=0", "size=10", "size=200", "size=abc", "size=", "page=0&size=5", "page=1&size=1000", "filter=archived", "sort=biggest", "variants=", "variants=other", "hash=notahash"}
|
|
for _, query := range queries {
|
|
t.Run(query, func(t *testing.T) {
|
|
env := newMediaEnv(t)
|
|
seedBulkImages(env, 3)
|
|
|
|
rec := env.send(t, http.MethodGet, "/v1/media/?"+url.PathEscape(query), "", mediaVerifiedUserId)
|
|
|
|
if rec.Code != http.StatusBadRequest || rec.Body.String() != apperr.MediaQueryErrorMessage+"\n" {
|
|
t.Errorf("unexpected response %d %q", rec.Code, rec.Body.String())
|
|
}
|
|
if env.images.listCalls != 0 {
|
|
t.Errorf("expected the repository not to be called")
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("Repository failure answers 500", func(t *testing.T) {
|
|
env := newMediaEnv(t)
|
|
env.images.fail.failNext("List")
|
|
|
|
rec := env.send(t, http.MethodGet, "/v1/media/", "", mediaVerifiedUserId)
|
|
|
|
if rec.Code != http.StatusInternalServerError || rec.Body.String() != "Internal server error\n" {
|
|
t.Errorf("unexpected response %d %q", rec.Code, rec.Body.String())
|
|
}
|
|
})
|
|
|
|
t.Run("An unauthenticated caller is refused as before", func(t *testing.T) {
|
|
env := newMediaEnv(t)
|
|
|
|
rec := env.send(t, http.MethodGet, "/v1/media/?page=1&size=5", "", 0)
|
|
|
|
if rec.Code != http.StatusUnauthorized || env.images.listCalls != 0 {
|
|
t.Errorf("unexpected response %d with %d calls", rec.Code, env.images.listCalls)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestListMediaUseCasePaging(t *testing.T) {
|
|
build := func() (*mockImageRepo, *mockVariantFileRepo, *useCases.ListMediaUseCase) {
|
|
images := newMockImageRepo()
|
|
variants := newMockVariantFileRepo()
|
|
return images, variants, useCases.NewListMediaUseCase(images, newMockArticleRepo(), newMockUserRepo(), variants, plainSites(), plainAuthors())
|
|
}
|
|
seed := func(images *mockImageRepo, count int) {
|
|
for id := uint32(1); id <= uint32(count); id++ {
|
|
images.images[id] = domainImage.Image{ID: id, Path: fmt.Sprintf("/static/uploadedImages/img-%d.jpg", id), Filename: "a.jpg", Size: int64(id), CreatedAt: mockImageEpoch.Add(time.Duration(id) * time.Minute)}
|
|
}
|
|
}
|
|
|
|
t.Run("List returns the items, total, page, size and counts", func(t *testing.T) {
|
|
images, _, useCase := build()
|
|
seed(images, 12)
|
|
query := domainImage.NewListQuery()
|
|
query.Page, query.Size = 3, 5
|
|
|
|
page, err := useCase.List(query)
|
|
|
|
if err != nil || len(page.Items) != 2 || page.Total != 12 || page.Page != 3 || page.Size != 5 || page.Counts.All != 12 {
|
|
t.Errorf("unexpected page %+v err %v", page, err)
|
|
}
|
|
})
|
|
|
|
t.Run("The variants filter and total_size sort apply before slicing", func(t *testing.T) {
|
|
images, variants, useCase := build()
|
|
seed(images, 12)
|
|
for _, id := range []uint32{3, 7, 9} {
|
|
_, _ = variants.Save(domainImage.VariantFile{VariantName: "hero", SourceName: domainImage.StoredName(images.images[id].Path), CachedName: fmt.Sprintf("c%d", id), ContentType: "image/jpeg", Size: int64(id) * 1000, Fingerprint: "f"})
|
|
}
|
|
with := domainImage.NewListQuery()
|
|
with.Variants, with.Sort, with.Size = domainImage.VariantsWith, domainImage.SortTotalSize, 5
|
|
|
|
page, err := useCase.List(with)
|
|
|
|
if err != nil || page.Total != 3 || len(page.Items) != 3 || page.Items[0].Image.ID != 9 || page.Counts.All != 12 {
|
|
t.Errorf("unexpected page %+v err %v", page, err)
|
|
}
|
|
|
|
all := domainImage.NewListQuery()
|
|
all.Sort, all.Size = domainImage.SortTotalSize, 5
|
|
first, _ := useCase.List(all)
|
|
if first.Items[0].Image.ID != 9 || first.Items[1].Image.ID != 7 || first.Items[2].Image.ID != 3 {
|
|
t.Errorf("unexpected total_size order %v", first.Items)
|
|
}
|
|
})
|
|
|
|
t.Run("A repository error is propagated", func(t *testing.T) {
|
|
images, _, useCase := build()
|
|
images.fail.failNextWith("List", errRepositoryFailure)
|
|
|
|
_, err := useCase.List(domainImage.NewListQuery())
|
|
|
|
if !errors.Is(err, errRepositoryFailure) {
|
|
t.Errorf("err = %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("ListQuery.Validate rejects a bad page or size", func(t *testing.T) {
|
|
for _, mutate := range []func(*domainImage.ListQuery){
|
|
func(q *domainImage.ListQuery) { q.Page = 0 },
|
|
func(q *domainImage.ListQuery) { q.Size = 0 },
|
|
func(q *domainImage.ListQuery) { q.Size = 10 },
|
|
func(q *domainImage.ListQuery) { q.Size = 101 },
|
|
} {
|
|
query := domainImage.NewListQuery()
|
|
mutate(&query)
|
|
if err := query.Validate(); !errors.Is(err, domainImage.ErrInvalidQuery) {
|
|
t.Errorf("err = %v for %+v", err, query)
|
|
}
|
|
}
|
|
for _, size := range []int{5, 25, 50, 100} {
|
|
query := domainImage.NewListQuery()
|
|
query.Size = size
|
|
if err := query.Validate(); err != nil {
|
|
t.Errorf("size %d: err = %v", size, err)
|
|
}
|
|
}
|
|
})
|
|
}
|