149 lines
7.5 KiB
Gherkin
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
|