feat(media): pagination
This commit is contained in:
1 parent
04dc151ed7
commit
63c1e7076e
65 files changed
+2960
-365
No files matched your search
@@ -0,0 +1,67 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"RenewCMS/internal/domain/image"
|
||||
)
|
||||
|
||||
func sortedIDs(order image.Sort, imgs []image.Image) []uint32 {
|
||||
image.SortImages(imgs, order)
|
||||
ids := make([]uint32, len(imgs))
|
||||
for i, img := range imgs {
|
||||
ids[i] = img.ID
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func equalIDs(a, b []uint32) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i := range a {
|
||||
if a[i] != b[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func TestImageSortTieBreaks(t *testing.T) {
|
||||
now := time.Now()
|
||||
build := func() []image.Image {
|
||||
return []image.Image{
|
||||
{ID: 2, Filename: "a.png", Size: 10, CreatedAt: now},
|
||||
{ID: 1, Filename: "a.png", Size: 10, CreatedAt: now},
|
||||
{ID: 3, Filename: "b.png", Size: 20, CreatedAt: now.Add(time.Hour)},
|
||||
}
|
||||
}
|
||||
cases := []struct {
|
||||
order image.Sort
|
||||
want []uint32
|
||||
}{
|
||||
{image.SortOldest, []uint32{1, 2, 3}},
|
||||
{image.SortNewest, []uint32{3, 2, 1}},
|
||||
{image.SortName, []uint32{1, 2, 3}},
|
||||
{image.SortLargest, []uint32{3, 1, 2}},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := sortedIDs(c.order, build()); !equalIDs(got, c.want) {
|
||||
t.Errorf("%s: got %v want %v", c.order, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestImageSortByTotalSizeTieBreak(t *testing.T) {
|
||||
imgs := []image.Image{
|
||||
{ID: 2, Path: "/u/x.png", Size: 10},
|
||||
{ID: 1, Path: "/u/y.png", Size: 10},
|
||||
{ID: 3, Path: "/u/z.png", Size: 11},
|
||||
}
|
||||
image.SortImagesByTotalSize(imgs, nil)
|
||||
got := []uint32{imgs[0].ID, imgs[1].ID, imgs[2].ID}
|
||||
if !equalIDs(got, []uint32{3, 1, 2}) {
|
||||
t.Fatalf("got %v", got)
|
||||
}
|
||||
}
|
||||
@@ -75,6 +75,7 @@ type mockImageRepo struct {
|
||||
images map[uint32]imageDomain.Image
|
||||
nextID uint32
|
||||
fail failSwitch
|
||||
listCalls int
|
||||
failDeleteFor map[uint32]error
|
||||
deleteCalls []uint32
|
||||
createCalls int
|
||||
@@ -169,6 +170,7 @@ func (m *mockImageRepo) Delete(id uint32) error {
|
||||
}
|
||||
|
||||
func (m *mockImageRepo) List(query imageDomain.SearchQuery) ([]imageDomain.Image, error) {
|
||||
m.listCalls++
|
||||
if err := m.fail.trigger("List"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -3,9 +3,6 @@ package test
|
||||
import (
|
||||
domainArticle "RenewCMS/internal/domain/article"
|
||||
"RenewCMS/internal/infrastructure/useCases"
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
@@ -20,38 +17,6 @@ func (r *mediaOrderRecordingArticleRepo) UpdateBody(id uint32, body string) (dom
|
||||
return r.mockArticleRepo.UpdateBody(id, body)
|
||||
}
|
||||
|
||||
func TestMediaCursorBoundaries(t *testing.T) {
|
||||
seed := func(env *mediaEnv) {
|
||||
env.seedImage(1, "a.jpg", 10, "image/jpeg", 3, 2, hashA, 1)
|
||||
env.seedImage(2, "b.jpg", 20, "image/jpeg", 3, 2, hashB, 1)
|
||||
}
|
||||
encode := func(raw string) string {
|
||||
return url.QueryEscape(base64.RawURLEncoding.EncodeToString([]byte(raw)))
|
||||
}
|
||||
|
||||
t.Run("A cursor at offset zero is accepted and starts from the first item", func(t *testing.T) {
|
||||
env := newMediaEnv(t)
|
||||
seed(env)
|
||||
|
||||
rec := env.send(t, http.MethodGet, "/v1/media/?cursor="+encode("offset:0"), "", mediaVerifiedUserId)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected status 200, got %d", rec.Code)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("A negative cursor offset is refused", func(t *testing.T) {
|
||||
env := newMediaEnv(t)
|
||||
seed(env)
|
||||
|
||||
rec := env.send(t, http.MethodGet, "/v1/media/?cursor="+encode("offset:-1"), "", mediaVerifiedUserId)
|
||||
|
||||
if rec.Code != http.StatusBadRequest {
|
||||
t.Fatalf("expected status 400, got %d", rec.Code)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestMediaMergeRewritesArticlesInIdOrder(t *testing.T) {
|
||||
env := newMediaEnv(t)
|
||||
env.seedImage(2, "floorboards.jpg", 20, "image/jpeg", 3, 2, hashB, 1)
|
||||
|
||||
@@ -32,6 +32,7 @@ const (
|
||||
type mediaEnv struct {
|
||||
articles *mockArticleRepo
|
||||
images *mockImageRepo
|
||||
variants *mockVariantFileRepo
|
||||
storage *mockImageStorage
|
||||
users *mockUserRepo
|
||||
purgeSettings *mockPurgeSettingsRepo
|
||||
@@ -48,6 +49,7 @@ func newMediaEnv(t *testing.T) *mediaEnv {
|
||||
env := &mediaEnv{
|
||||
articles: newMockArticleRepo(),
|
||||
images: newMockImageRepo(),
|
||||
variants: newMockVariantFileRepo(),
|
||||
storage: newMockImageStorage(),
|
||||
users: newMockUserRepo(),
|
||||
token: jwtauth.New("HS256", []byte("test-secret"), nil),
|
||||
@@ -72,7 +74,7 @@ func newMediaEnv(t *testing.T) *mediaEnv {
|
||||
GetUserUseCase: useCases.NewGetUserUseCase(env.users),
|
||||
CreateImageUseCase: useCases.NewCreateImageUseCase(env.images, env.storage, discardRecorder),
|
||||
GenerateVariantsOnUploadUseCase: newInertGenerateOnUploadUseCase(),
|
||||
ListMediaUseCase: useCases.NewListMediaUseCase(env.images, env.articles, env.users, newMockVariantFileRepo()),
|
||||
ListMediaUseCase: useCases.NewListMediaUseCase(env.images, env.articles, env.users, env.variants),
|
||||
RenameMediaUseCase: useCases.NewRenameMediaUseCase(env.images, discardRecorder),
|
||||
DeleteMediaUseCase: useCases.NewDeleteMediaUseCase(env.images, env.storage, newMockVariantFileRepo(), discardRecorder),
|
||||
MergeMediaUseCase: merge,
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os/exec"
|
||||
"reflect"
|
||||
"regexp"
|
||||
@@ -35,9 +34,11 @@ func seedListingLibrary(env *mediaEnv) {
|
||||
}
|
||||
|
||||
type listBody struct {
|
||||
Items []map[string]any `json:"items"`
|
||||
NextCursor *string `json:"next_cursor"`
|
||||
Counts map[string]int `json:"counts"`
|
||||
Items []map[string]any `json:"items"`
|
||||
Total int `json:"total"`
|
||||
Page int `json:"page"`
|
||||
Size int `json:"size"`
|
||||
Counts map[string]int `json:"counts"`
|
||||
}
|
||||
|
||||
func (e *mediaEnv) list(t *testing.T, query string) (listBody, string) {
|
||||
@@ -155,11 +156,11 @@ func TestMediaListing(t *testing.T) {
|
||||
if !strings.Contains(raw, `"width":null`) || !strings.Contains(raw, `"height":null`) {
|
||||
t.Errorf("expected null width and height in the raw body, got %s", raw)
|
||||
}
|
||||
if got.NextCursor != nil {
|
||||
t.Errorf("expected next_cursor null, got %v", *got.NextCursor)
|
||||
if strings.Contains(raw, "next_cursor") {
|
||||
t.Errorf("expected no next_cursor in the raw body")
|
||||
}
|
||||
if !strings.Contains(raw, `"next_cursor":null`) {
|
||||
t.Errorf("expected next_cursor null in the raw body")
|
||||
if got.Total != 5 || got.Page != 1 || got.Size != 25 {
|
||||
t.Errorf("unexpected paging %d %d %d", got.Total, got.Page, got.Size)
|
||||
}
|
||||
if !reflect.DeepEqual(got.Counts, map[string]int{"all": 5, "unused": 3, "duplicates": 1}) {
|
||||
t.Errorf("unexpected counts %v", got.Counts)
|
||||
@@ -293,31 +294,26 @@ func TestMediaListing(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("limit and cursor page through the list", func(t *testing.T) {
|
||||
t.Run("page and size page through the list", func(t *testing.T) {
|
||||
env := newMediaEnv(t)
|
||||
seedListingLibrary(env)
|
||||
|
||||
first, _ := env.list(t, "?limit=2")
|
||||
expectIds(t, idsOf(first.Items), []uint32{5, 4})
|
||||
if first.NextCursor == nil || *first.NextCursor == "" {
|
||||
t.Fatalf("expected a next_cursor on page 1")
|
||||
for id := uint32(1); id <= 12; id++ {
|
||||
env.seedImage(id, "image.jpg", 10, "image/jpeg", 3, 2, hashA, 1)
|
||||
}
|
||||
|
||||
second, _ := env.list(t, "?limit=2&cursor="+url.QueryEscape(*first.NextCursor))
|
||||
expectIds(t, idsOf(second.Items), []uint32{3, 2})
|
||||
if second.NextCursor == nil || *second.NextCursor == "" {
|
||||
t.Fatalf("expected a next_cursor on page 2")
|
||||
}
|
||||
first, _ := env.list(t, "?size=5&page=1")
|
||||
second, _ := env.list(t, "?size=5&page=2")
|
||||
third, _ := env.list(t, "?size=5&page=3")
|
||||
|
||||
third, _ := env.list(t, "?limit=2&cursor="+url.QueryEscape(*second.NextCursor))
|
||||
expectIds(t, idsOf(third.Items), []uint32{1})
|
||||
if third.NextCursor != nil {
|
||||
t.Errorf("expected next_cursor null on the last page, got %v", *third.NextCursor)
|
||||
expectIds(t, idsOf(first.Items), []uint32{12, 11, 10, 9, 8})
|
||||
expectIds(t, idsOf(second.Items), []uint32{7, 6, 5, 4, 3})
|
||||
expectIds(t, idsOf(third.Items), []uint32{2, 1})
|
||||
if third.Total != 12 || third.Page != 3 || third.Size != 5 {
|
||||
t.Errorf("unexpected paging %d %d %d", third.Total, third.Page, third.Size)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("A malformed query answers 400", func(t *testing.T) {
|
||||
for _, query := range []string{"filter=bogus", "sort=bogus", "limit=0", "limit=101", "limit=abc", "cursor=%%%", "cursor=bogus", "hash=notahash"} {
|
||||
for _, query := range []string{"filter=bogus", "sort=bogus", "page=0", "page=abc", "size=10", "size=200", "size=", "hash=notahash"} {
|
||||
t.Run(query, func(t *testing.T) {
|
||||
env := newMediaEnv(t)
|
||||
seedListingLibrary(env)
|
||||
|
||||
@@ -0,0 +1,349 @@
|
||||
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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"RenewCMS/internal/domain/article"
|
||||
domain "RenewCMS/internal/domain/image"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -184,16 +183,15 @@ func TestMediaListVariantsFilterAndSort(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("variants=with and sort=total_size page through the list", func(t *testing.T) {
|
||||
t.Run("variants=with and sort=total_size answer the ordered page with its total", func(t *testing.T) {
|
||||
env := newVariantEnv(t)
|
||||
env.seedLibrary()
|
||||
|
||||
first, firstBody := env.listIds("?variants=with&sort=total_size&limit=2")
|
||||
cursor, _ := firstBody["next_cursor"].(string)
|
||||
second, secondBody := env.listIds("?variants=with&sort=total_size&limit=2&cursor=" + url.QueryEscape(cursor))
|
||||
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(first, []float64{1, 4}) || cursor == "" || !slices.Equal(second, []float64{2}) || secondBody["next_cursor"] != nil {
|
||||
t.Errorf("first = %v (%q), second = %v (%v)", first, cursor, second, secondBody["next_cursor"])
|
||||
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)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
Reference in new issue
Block a user