Files
RenewCMS/features/media_pagination_api.feature
T

149 lines
7.5 KiB
Gherkin

Feature: Paginated media listing
As an API consumer, the RenewCMS media page or the editor picker
I want GET /v1/media/ to answer one page of images with the total and the library-wide counts
So that large libraries load quickly and every consumer pages the same way
Background:
Given GET /v1/media/ is mounted inside the jwtauth.Verifier + Authenticator group of api/router.go, unchanged
And BREAKING CHANGE: the body is {"items": [...], "total": N, "page": P, "size": S, "counts": {"all": A, "unused": U, "duplicates": D}} in snake_case; next_cursor is removed and no X-Total-Count header is sent
And each item is the existing media item (id, filename, url, size, content_type, width, height, sha256, uploaded_by, created_at, used_in, duplicate, variant_count, variant_size) and items is [] rather than null
And paging follows the Activity and Articles convention: page is an integer >= 1 (default 1), size is one of 5, 25, 50, 100 (default 25)
And q, hash, filter (all, unused, duplicates), sort (newest, oldest, name, largest, total_size) and variants (with, without) keep their current meaning and validation; the default order is newest
And total is the number of images matching q, hash, filter and variants, before paging; page and size echo the values applied
And counts stays library-wide: it ignores q, hash, filter, variants and paging
And ordering is applied to the whole matching list before slicing, with id as the final tie-break, so pages never overlap or skip (newest and total_size are made deterministic; oldest, name and largest already tie on id)
And a page beyond the last answers 200 with items [] and the real total
And any invalid page, size, filter, sort, variants or hash answers 400 with the plain-text body "The media query is invalid." (apperr.MediaQueryErrorMessage); page and size are validated with validate tags through validator.Validate.Struct
And cursor and limit are no longer parameters: they are ignored, never an error, and have no effect on paging
And ListMediaUseCase reaches data only through domain interfaces; the domain ListQuery carries Page and Size instead of Cursor and Limit; the handler never touches a repository
And the SPA sends explicit page and size on every list call
Scenario: A bare request answers the envelope with the defaults
Given the library holds 30 images
When a verified client sends GET /v1/media/
Then the status is 200
And the body has page 1, size 25, total 30, 25 items and counts {"all": 30, "unused": U, "duplicates": D}
And the body has no next_cursor key
Scenario Outline: Defaults when only one parameter is given
Given the library holds 60 images
When a verified client sends GET /v1/media/?<query>
Then the body has page <page>, size <size>, total 60 and <count> items
Examples:
| query | page | size | count |
| 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 |
Scenario: Pages partition the list with no overlap
Given the library holds 12 images
When a client requests ?page=1&size=5, ?page=2&size=5 and ?page=3&size=5
Then the pages hold 5, 5 and 2 items, total is 12 on each, and no id repeats
Scenario: Newest first with the id tie-break
Given images id 1 uploaded 2026-09-01, id 2 and id 3 uploaded 2026-09-03
When a client sends GET /v1/media/?size=5
Then the item ids are 3, 2, 1
Scenario Outline: Sort orders the whole library before paging
Given 12 images with distinct names, sizes and dates
When a client requests ?sort=<sort>&size=5 for pages 1, 2 and 3
Then the pages hold the whole ordered list in <order> with no repeat
Examples:
| sort | order |
| newest | created_at descending |
| oldest | created_at ascending |
| name | filename stem, case-insensitive ascending |
| largest | size descending |
| total_size | original size plus generated variant bytes, descending |
Scenario: Filter and variants narrow total and items but not the counts
Given 40 images of which 6 are unused and 3 hold generated variants, and the library counts are {"all": 40, "unused": 6, "duplicates": 2}
When a client sends GET /v1/media/?filter=unused&size=5
Then total is 6, the page holds 5 items and counts is {"all": 40, "unused": 6, "duplicates": 2}
When a client sends GET /v1/media/?variants=with
Then total is 3 and counts is unchanged
Scenario: Search combines with paging
Given 7 images whose filename contains "wall" and 20 others
When a client sends GET /v1/media/?q=wall&size=5&page=2
Then total is 7, the page holds 2 items and counts.all is 27
Scenario: Hash lookup returns every copy in one page
Given 3 images share the sha256 H
When a client sends GET /v1/media/?hash=H&page=1&size=100
Then total is 3 and items has the 3 copies
Scenario: Hash lookup with no paging uses the defaults
Given 3 images share the sha256 H
When a client sends GET /v1/media/?hash=H
Then the body has page 1, size 25, total 3 and 3 items
Scenario: A page beyond the last is empty with the real total
Given the library holds 12 images
When a client sends GET /v1/media/?page=99&size=5
Then the status is 200, items is [], total is 12, page is 99, size is 5 and counts.all is 12
Scenario: An empty library
Given the library is empty
When a client sends GET /v1/media/
Then the status is 200, items is [], total is 0 and counts is {"all": 0, "unused": 0, "duplicates": 0}
Scenario: counts ignores every filter and the page
Given the library holds 30 images
When a client sends GET /v1/media/?q=zzzz&page=3&size=5
Then items is [], total is 0 and counts.all is 30
Scenario Outline: cursor and limit are ignored
Given the library holds 30 images
When a client sends GET /v1/media/?<query>
Then the status is 200 and the body has page 1, size 25 and 25 items
Examples:
| query |
| cursor=abc |
| limit=5 |
| limit=0 |
| limit=abc |
| cursor=b2Zmc2V0OjU |
Scenario Outline: Invalid parameters answer 400
When a client sends GET /v1/media/?<query>
Then the status is 400
And the body is "The media query is invalid."
And the repository was not called
Examples:
| query |
| 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 |
Scenario: Repository failure answers 500
Given the repository fails on its next call
When a client sends GET /v1/media/
Then the status is 500 and the body is "Internal server error"
Scenario: An unauthenticated caller is refused as before
When a client sends GET /v1/media/?page=1&size=5 without a valid jwt cookie
Then the response is the unchanged 401 of the auth group and the use case is not called