515 lines
31 KiB
Gherkin
515 lines
31 KiB
Gherkin
Feature: Image variants per image: library fields, per-image routes and generate on upload
|
|
|
|
As an owner or admin of a RenewCMS site
|
|
I want to see, generate and purge the variants of one image, and to have variants generated when I upload
|
|
So that the media screens show what is cached for each image, and the activity log records when
|
|
I generate all the variants of an image or purge them
|
|
|
|
Background:
|
|
Given the spec of record is docs/design/screens/image-variants.md sections 3, 4, 6 and 8 and its "Back-office API errors" table
|
|
And the routes the design marks "proposed" are fixed here, and features/image-variants.feature is the contract for the variants themselves
|
|
And this slice is API only: the SPA is in features/image_variants_media_screens.feature and features/image_variants_editor_picker.feature, docs/design is unchanged and the public serving of ?v=<name> is unchanged
|
|
And it excludes the Settings → Media → Cache routes (GET /v1/image-variants/cache, POST /v1/image-variants/cache/purge)
|
|
And the stored name of an image is image.StoredName(Path), the public url of one of its variants is "/static/uploadedImages/<storedName>?v=<name>", and a variant file is generated by the same use case path as a public request (same limits, same render slots, same singleflight key, same index row), so a file generated here is a cache hit for the public url and the other way round
|
|
And a variant file counts as generated for an image only when its index row exists and carries the variant's current settings fingerprint
|
|
And internal/domain/image gains the sentinels ErrVariantSourceUnsupported (an SVG source) and ErrVariantSourceUnreadable (the source cannot be decoded), asserted with errors.Is, and reuses ErrSourceTooLarge, ErrVariantNotFound and ErrImageNotFound
|
|
And internal/domain/image gains one pure function returning the output width and height for a source size and a variant's settings, following the table of features/image-variants.feature "Output geometry never upscales", and both the renderer and the per-image listing use it
|
|
And image.VariantFileRepository gains one method returning, for every source at once, the number of index rows and their total bytes keyed by source stored name, so a library page never queries per image
|
|
And internal/domain/audit gains the actions media.variant_generate_all and media.variant_purge_image (category "media", target {"type": "image", "id": "<image id>", "name": "<filename>"}) and the params generated, failed and causes
|
|
And the new routes are mounted in api/router.go inside NewMediaRouter, in the group behind middleware.RequireOwnerOrAdmin, so a request with an API key has no user_id claim and answers 401
|
|
And request validation, when there is a body, goes through validate tags and validator.Validate.Struct, and error responses are plain text from http.Error
|
|
And new apperr constants are VariantSourceUnsupportedErrorMessage = "Variants aren't generated for this format." and VariantSourceUnreadableErrorMessage = "The source image can't be read, so this variant can't be generated.", and the existing VariantSourceTooLargeErrorMessage = "The source image is too large to generate this variant." is reused
|
|
And a per-image variant is JSON {"name", "width", "height", "fit", "format", "output_width", "output_height", "output_format", "generated", "size", "url"} where width and height are the variant's settings (null when auto), output_* are the dimensions and format the file has or will have, generated is a boolean, size is the file size in bytes or null when not generated, and url is the public url above
|
|
And a generation failure cause is one of "too_large" (ErrSourceTooLarge), "unsupported_format" (ErrVariantSourceUnreadable) or "failed" (anything else)
|
|
And every library item gains the integers variant_count and variant_size (the number of index rows of that image whatever the variant's state, and their bytes), both 0 for an image with no file
|
|
|
|
Rule: Only owners and admins reach the per-image variant routes
|
|
|
|
Scenario Outline: Every per-image variant route is guarded
|
|
When the client sends <method> <path> with <credentials>
|
|
Then the response status is <status>
|
|
And the plain text body is <body>
|
|
And no variant, index row, blob or activity event is read or changed
|
|
|
|
Examples:
|
|
| method | path | credentials | status | body |
|
|
| GET | /v1/media/1/variants | no cookie | 401 | apperr.UnauthorizedErrorMessage |
|
|
| GET | /v1/media/1/variants | a stale token_version cookie | 401 | apperr.UnauthorizedErrorMessage |
|
|
| GET | /v1/media/1/variants | the cookie of an unverified user | 403 | apperr.VerificationRequiredErrorMessage |
|
|
| GET | /v1/media/1/variants | the cookie of a plain user | 403 | apperr.OwnerOrAdminRequiredErrorMessage |
|
|
| POST | /v1/media/1/variants | the cookie of a plain user | 403 | apperr.OwnerOrAdminRequiredErrorMessage |
|
|
| POST | /v1/media/1/variants/hero | the cookie of a plain user | 403 | apperr.OwnerOrAdminRequiredErrorMessage |
|
|
| DELETE | /v1/media/1/variants/hero | the cookie of a plain user | 403 | apperr.OwnerOrAdminRequiredErrorMessage |
|
|
| GET | /v1/media/1/variants | a valid API key | 401 | apperr.UnauthorizedErrorMessage |
|
|
| POST | /v1/media/1/variants | a valid API key | 401 | apperr.UnauthorizedErrorMessage |
|
|
|
|
Scenario Outline: Owners and admins are both allowed
|
|
When <role> sends GET /v1/media/1/variants with a valid cookie
|
|
Then the response status is 200
|
|
|
|
Examples:
|
|
| role |
|
|
| the owner |
|
|
| an admin |
|
|
|
|
Scenario Outline: A malformed id or an unknown image
|
|
When an admin sends <method> /v1/media/<id>/variants<suffix>
|
|
Then the response status is <status> and the plain text body is <body>
|
|
|
|
Examples:
|
|
| method | id | suffix | status | body |
|
|
| GET | abc | | 400 | apperr.IdUint32ErrorMessage |
|
|
| POST | abc | | 400 | apperr.IdUint32ErrorMessage |
|
|
| GET | 9999 | | 404 | Image not found |
|
|
| POST | 9999 | | 404 | Image not found |
|
|
| POST | 9999 | /hero | 404 | Image not found |
|
|
| DELETE | 9999 | /hero | 404 | Image not found |
|
|
|
|
Rule: Listing the variants of one image
|
|
|
|
Background:
|
|
Given image 1 "north-wall.jpg" is a 4032 x 3024 JPEG of 4089446 bytes stored as "a1b2.jpg"
|
|
And image 2 "logo.svg" is an SVG stored as "e5f6.svg"
|
|
And the variants, in name order: "card" (inside, 800 x auto, webp, 80), "hero" (cover, 1200 x 630, webp, 82), "og" (cover, 1200 x 630, jpeg, 80), "print" (inside, auto x 2400, png, disabled), "square" (cover, 600 x 600, jpeg, 85), "thumb" (cover, 400 x 300, webp, 80), all enabled unless said
|
|
And the index holds current rows for a1b2.jpg: thumb 18432 bytes, card 65536, hero 98304, square 83968, and a row for the disabled "print" of 3250586 bytes
|
|
|
|
Scenario: The enabled variants, in name order, with their output and what is cached
|
|
When an admin sends GET /v1/media/1/variants
|
|
Then the response status is 200
|
|
And "unsupported" is null
|
|
And "items" are card, hero, og, square, thumb, in that order, and "print" is absent
|
|
And the items read:
|
|
| name | width | height | output_width | output_height | output_format | generated | size |
|
|
| card | 800 | null | 800 | 600 | webp | true | 65536 |
|
|
| hero | 1200 | 630 | 1200 | 630 | webp | true | 98304 |
|
|
| og | 1200 | 630 | 1200 | 630 | jpeg | false | null |
|
|
| square | 600 | 600 | 600 | 600 | jpeg | true | 83968 |
|
|
| thumb | 400 | 300 | 400 | 300 | webp | true | 18432 |
|
|
And the url of "thumb" is "/static/uploadedImages/a1b2.jpg?v=thumb"
|
|
|
|
Scenario: A row whose fingerprint is stale is not generated
|
|
Given the index row of "hero" for a1b2.jpg carries the fingerprint of an older quality
|
|
When an admin sends GET /v1/media/1/variants
|
|
Then "hero" has generated false and size null
|
|
|
|
Scenario Outline: A variant in the original format takes the format of the source
|
|
Given a variant "keep" with format original over a source of content type <content_type> and webp encoding <webp>
|
|
When an admin sends GET /v1/media/<id>/variants
|
|
Then "keep" has output_format "<output_format>"
|
|
|
|
Examples:
|
|
| content_type | webp | output_format |
|
|
| image/jpeg | available | jpeg |
|
|
| image/png | available | png |
|
|
| image/webp | available | webp |
|
|
| image/webp | unavailable | png |
|
|
|
|
Scenario Outline: The output size never exceeds the source
|
|
Given a source of <sw> x <sh> pixels and a variant with width <w>, height <h> and fit <fit>
|
|
When an admin sends GET /v1/media/<id>/variants
|
|
Then the variant has output_width <ow> and output_height <oh>
|
|
And they equal the dimensions the renderer produces for the same source and settings
|
|
|
|
Examples:
|
|
| sw | sh | w | h | fit | ow | oh |
|
|
| 4032 | 3024 | 800 | | cover | 800 | 600 |
|
|
| 4032 | 3024 | | 2400 | inside | 3200 | 2400 |
|
|
| 4032 | 3024 | 8000 | 8000 | cover | 3024 | 3024 |
|
|
| 300 | 200 | 800 | | cover | 300 | 200 |
|
|
| 300 | 200 | 400 | 300 | cover | 267 | 200 |
|
|
| 300 | 200 | 400 | 300 | contain | 400 | 300 |
|
|
| 300 | 200 | 1200 | 1200 | inside | 300 | 200 |
|
|
|
|
Scenario: A source that no variant can be generated for answers an empty list
|
|
When an admin sends GET /v1/media/2/variants
|
|
Then the response status is 200 and the body is {"unsupported": "svg", "items": []}
|
|
|
|
Scenario: No enabled variant answers an empty array, not null
|
|
Given every variant is disabled
|
|
When an admin sends GET /v1/media/1/variants
|
|
Then the response status is 200 and the raw body holds "items":[] and "unsupported":null
|
|
|
|
Scenario: Listing reads nothing it should not change
|
|
When an admin sends GET /v1/media/1/variants
|
|
Then no blob is opened, no render happens, no row is written and no activity event is recorded
|
|
|
|
Rule: Generating one variant of one image
|
|
|
|
Background:
|
|
Given image 1 "north-wall.jpg" is a 1000 x 800 PNG stored as "a1b2.png"
|
|
And the variants "thumb" (cover 400 x 300 jpeg 80, enabled), "hero" (cover 1200 x 630 jpeg 82, enabled) and "off" (inside 300 x auto png, disabled)
|
|
And no generated file exists yet
|
|
|
|
Scenario: The first generation renders, stores and indexes the file
|
|
When an admin sends POST /v1/media/1/variants/thumb
|
|
Then the response status is 200
|
|
And the body is the per-image variant "thumb" with generated true, output 400 x 300, output_format "jpeg" and size equal to the stored blob's size
|
|
And exactly one blob was put in image.Storage and one index row exists for thumb and a1b2.png
|
|
And the original blob a1b2.png is unchanged
|
|
And no activity event is recorded
|
|
|
|
Scenario: The public url then serves that file without rendering
|
|
Given POST /v1/media/1/variants/thumb answered 200
|
|
When the public client sends GET /static/uploadedImages/a1b2.png?v=thumb
|
|
Then the body is byte-identical to the stored blob and the renderer was called once in total
|
|
|
|
Scenario: Generating a variant that is already generated renders nothing
|
|
Given POST /v1/media/1/variants/thumb answered 200
|
|
When an admin sends POST /v1/media/1/variants/thumb again
|
|
Then the response status is 200 with the same size and the renderer was called once in total
|
|
|
|
Scenario: A public request and an admin request at the same time render once
|
|
When the public client and an admin request thumb for a1b2.png concurrently while the render blocks
|
|
Then the renderer is called once and image.Storage Put is called once
|
|
|
|
Scenario Outline: A request that cannot be served
|
|
When an admin sends POST /v1/media/<id>/variants/<name>
|
|
Then the response status is <status> and the plain text body is <body>
|
|
And nothing is rendered and nothing is stored
|
|
|
|
Examples:
|
|
| id | name | status | body |
|
|
| 1 | missing | 404 | Variant not found |
|
|
| 1 | off | 404 | Variant not found |
|
|
| 1 | Thumb | 404 | Variant not found |
|
|
|
|
Scenario: An SVG answers 422
|
|
Given image 2 is an SVG stored as "e5f6.svg"
|
|
When an admin sends POST /v1/media/2/variants/thumb
|
|
Then the response status is 422 and the plain text body is apperr.VariantSourceUnsupportedErrorMessage
|
|
And nothing is rendered and nothing is stored
|
|
|
|
Scenario Outline: A source beyond a cap answers 413
|
|
Given <cap> is lower than the source
|
|
When an admin sends POST /v1/media/1/variants/thumb
|
|
Then the response status is 413 and the plain text body is apperr.VariantSourceTooLargeErrorMessage
|
|
And nothing is stored
|
|
|
|
Examples:
|
|
| cap |
|
|
| IMAGE_VARIANT_MAX_SOURCE_BYTES |
|
|
| IMAGE_VARIANT_MAX_SOURCE_PIXELS |
|
|
|
|
Scenario: A source that cannot be decoded answers 415
|
|
Given the stored blob a1b2.png holds bytes that are not an image
|
|
When an admin sends POST /v1/media/1/variants/thumb
|
|
Then the response status is 415 and the plain text body is apperr.VariantSourceUnreadableErrorMessage
|
|
And nothing is stored
|
|
|
|
Scenario: Any other render failure is a generic 500 and is not cached
|
|
Given the renderer fails for its next call
|
|
When an admin sends POST /v1/media/1/variants/thumb twice
|
|
Then the first response is 500 with the body "Internal server error" and no blob or row exists
|
|
And the second response is 200
|
|
|
|
Scenario: A generation does not change the original, the other variants or the library list
|
|
When an admin sends POST /v1/media/1/variants/thumb
|
|
Then the stored image record, the other variants and the index rows of other images are unchanged
|
|
|
|
Rule: Generating every missing variant of one image
|
|
|
|
Background:
|
|
Given image 1 "north-wall.jpg" is a 4032 x 3024 JPEG stored as "a1b2.jpg"
|
|
And the enabled variants "card", "hero", "og", "square" and "thumb" and the disabled variant "print"
|
|
And no generated file exists yet
|
|
|
|
Scenario: Generate all renders every enabled variant and answers the refreshed list
|
|
When an admin sends POST /v1/media/1/variants
|
|
Then the response status is 200
|
|
And "generated" is 5, "failed" is 0 and "failures" is an empty array
|
|
And "items" are the five enabled variants in name order, all with generated true and their sizes
|
|
And exactly five blobs and five index rows exist and "print" has none
|
|
|
|
Scenario: Variants already generated are skipped and not counted
|
|
Given "card" and "thumb" are already generated
|
|
When an admin sends POST /v1/media/1/variants
|
|
Then "generated" is 3, the renderer was called three times in total beyond the earlier two, and all five items have generated true
|
|
|
|
Scenario Outline: A failing variant is reported and the others are still generated
|
|
Given generating "hero" fails with <failure>
|
|
When an admin sends POST /v1/media/1/variants
|
|
Then the response status is 200
|
|
And "generated" is 4, "failed" is 1 and "failures" is [{"name": "hero", "cause": "<cause>"}]
|
|
And the "hero" item has generated false and the other four have generated true
|
|
|
|
Examples:
|
|
| failure | cause |
|
|
| ErrSourceTooLarge | too_large |
|
|
| ErrVariantSourceUnreadable | unsupported_format |
|
|
| a renderer error that wraps no sentinel | failed |
|
|
|
|
Scenario: Nothing to generate answers zero
|
|
Given all five enabled variants are generated
|
|
When an admin sends POST /v1/media/1/variants
|
|
Then the response status is 200, "generated" is 0 and "failed" is 0 and the renderer is not called
|
|
|
|
Scenario: No enabled variant answers zero
|
|
Given every variant is disabled
|
|
When an admin sends POST /v1/media/1/variants
|
|
Then the response status is 200, "generated" is 0, "failed" is 0 and "items" is an empty array
|
|
|
|
Scenario: An SVG answers 422
|
|
Given image 2 is an SVG
|
|
When an admin sends POST /v1/media/2/variants
|
|
Then the response status is 422 and the plain text body is apperr.VariantSourceUnsupportedErrorMessage
|
|
|
|
Rule: Purging the generated files of one variant of one image
|
|
|
|
Background:
|
|
Given image 1 stored as "a1b2.jpg" and image 3 stored as "c3d4.jpg"
|
|
And "thumb" and "hero" are generated for both images, thumb files of 18432 bytes and hero files of 98304 bytes
|
|
|
|
Scenario: Purging a generated variant removes its file and its row
|
|
When an admin sends DELETE /v1/media/1/variants/thumb
|
|
Then the response status is 200 and the body is {"purged_files": 1, "purged_bytes": 18432}
|
|
And the thumb blob of image 1 is deleted from image.Storage and its index row is gone
|
|
And the hero file of image 1 and both files of image 3 are untouched
|
|
|
|
Scenario: The next public request generates the file again
|
|
Given DELETE /v1/media/1/variants/thumb answered 200
|
|
When the public client sends GET /static/uploadedImages/a1b2.jpg?v=thumb
|
|
Then the response is 200 and a new blob was put
|
|
|
|
Scenario: Purging a variant that is not generated answers zero
|
|
Given the "thumb" file of image 1 is not generated
|
|
When an admin sends DELETE /v1/media/1/variants/thumb
|
|
Then the response status is 200 and the body is {"purged_files": 0, "purged_bytes": 0}
|
|
|
|
Scenario: A disabled variant keeps its files and they can be purged
|
|
Given "thumb" is disabled
|
|
When an admin sends DELETE /v1/media/1/variants/thumb
|
|
Then the response status is 200 and "purged_files" is 1
|
|
|
|
Scenario: An unknown variant answers 404
|
|
When an admin sends DELETE /v1/media/1/variants/missing
|
|
Then the response status is 404 and the plain text body is "Variant not found"
|
|
|
|
Scenario: A blob that cannot be deleted keeps its row and the call fails
|
|
Given image.Storage fails to delete the thumb blob of image 1
|
|
When an admin sends DELETE /v1/media/1/variants/thumb
|
|
Then the response status is 500 with the body "Internal server error" and the index row is kept
|
|
|
|
Rule: Variants flagged "generate on upload" are generated when an image is uploaded
|
|
|
|
Background:
|
|
Given the variants "thumb" (generate_on_upload true, enabled), "hero" (generate_on_upload true, enabled), "og" (generate_on_upload true, disabled) and "card" (generate_on_upload false, enabled)
|
|
And image.Storage and the renderer are fakes
|
|
|
|
Scenario: Flagged and enabled variants are generated before the response
|
|
When the owner sends POST /v1/media with a valid 1000 x 800 PNG
|
|
Then the response status is 201
|
|
And the index holds one row for "thumb" and one for "hero" for the new stored name, and none for "og" or "card"
|
|
And the item has variant_count 2 and variant_size equal to the two row sizes
|
|
And exactly one activity event exists, the "media.upload" event, and no variant event
|
|
|
|
Scenario: A failing variant never fails the upload
|
|
Given the renderer fails when generating "hero"
|
|
When the owner uploads a valid PNG
|
|
Then the response status is 201, "thumb" is generated, "hero" is not, and variant_count is 1
|
|
And the image exists and the failure is written to the process log with the variant name and the image id
|
|
|
|
Scenario: A source beyond a cap still uploads
|
|
Given IMAGE_VARIANT_MAX_SOURCE_PIXELS is lower than the new image
|
|
When the owner uploads it
|
|
Then the response status is 201 and variant_count is 0
|
|
|
|
Scenario: An SVG upload generates nothing
|
|
When the owner uploads a valid SVG
|
|
Then the response status is 201, the renderer is not called and variant_count is 0
|
|
|
|
Scenario: No flagged variant means no render
|
|
Given no variant has generate_on_upload true
|
|
When the owner uploads a valid PNG
|
|
Then the renderer is not called
|
|
|
|
Scenario: A refused upload generates nothing
|
|
When the owner uploads a PDF
|
|
Then the response status is 415 and the renderer is not called
|
|
|
|
Scenario Outline: Changing the flag does not backfill existing images
|
|
Given the library already holds image 1 with no generated file
|
|
When an admin <action>
|
|
Then the renderer is not called and image 1 has no generated file
|
|
|
|
Examples:
|
|
| action |
|
|
| creates the variant "wide" with generate_on_upload true |
|
|
| sends PUT /v1/image-variants/card with generate_on_upload true |
|
|
|
|
Scenario: Uploading the same bytes again generates files for the new image only
|
|
Given the library holds image 1 with generated files
|
|
When the owner uploads the same bytes again
|
|
Then the new image has its own generated files and image 1's files are unchanged
|
|
|
|
Rule: The library list carries the variant count and size and can filter and sort on them
|
|
|
|
Background:
|
|
Given the library of features/media_library.feature "Listing" (images 1 to 5, newest is 5) with these sizes and index rows:
|
|
| id | filename | size | variant files | variant bytes | total |
|
|
| 1 | north-wall.jpg | 4089446 | 4 | 266240 | 4355686 |
|
|
| 2 | floorboards.jpg | 2516582 | 1 | 41000 | 2557582 |
|
|
| 3 | floorboards-copy.jpg | 2516582 | 0 | 0 | 2516582 |
|
|
| 4 | window-frame.jpg | 812345 | 3 | 3000000 | 3812345 |
|
|
| 5 | logo.svg | 1204 | 0 | 0 | 1204 |
|
|
And the rows of image 4 belong to a disabled variant
|
|
And articles use the images as in that feature, so images 3, 4 and 5 are unused
|
|
|
|
Scenario: Every item carries variant_count and variant_size
|
|
When the client sends GET /v1/media/ with a valid cookie
|
|
Then item 1 has variant_count 4 and variant_size 266240
|
|
And item 4 has variant_count 3 and variant_size 3000000, although its variant is disabled
|
|
And items 3 and 5 have variant_count 0 and variant_size 0
|
|
And the response order, used_in, duplicate and counts are those of the existing feature
|
|
|
|
Scenario: The list reads the totals of all sources in one call
|
|
When the client sends GET /v1/media/ with a valid cookie
|
|
Then the variant file repository's per-source totals method was called once
|
|
|
|
Scenario: A new upload and a rename answer the same fields
|
|
When the owner renames image 1 to "north-wall-01"
|
|
Then the response item has variant_count 4 and variant_size 266240
|
|
|
|
Scenario Outline: variants narrows the list and the counts stay library-wide
|
|
When the client sends GET /v1/media/?variants=<variants> with a valid cookie
|
|
Then the "items" ids, as a set, are <ids>
|
|
And "counts" is still {"all": 5, "unused": 3, "duplicates": 1}
|
|
|
|
Examples:
|
|
| variants | ids |
|
|
| with | 1, 2, 4 |
|
|
| without | 3, 5 |
|
|
|
|
Scenario: variants, filter, q and sort combine
|
|
When the client sends GET /v1/media/?variants=with&filter=unused with a valid cookie
|
|
Then the "items" ids are 4
|
|
When the client sends GET /v1/media/?variants=with&q=floorboards with a valid cookie
|
|
Then the "items" ids are 2
|
|
|
|
Scenario: sort=total_size orders by original plus variants, largest first, then by id
|
|
When the client sends GET /v1/media/?sort=total_size with a valid cookie
|
|
Then the "items" ids are 1, 4, 2, 3, 5 in that order
|
|
|
|
Scenario: sort=largest is unchanged and sorts by the original alone
|
|
When the client sends GET /v1/media/?sort=largest with a valid cookie
|
|
Then the "items" ids are 1, 2, 3, 4, 5 in that order
|
|
|
|
Scenario: Equal totals fall back to the lowest id first
|
|
Given images 2 and 3 have equal totals
|
|
When the client sends GET /v1/media/?sort=total_size with a valid cookie
|
|
Then image 2 comes before image 3
|
|
|
|
Scenario: variants=with and sort=total_size page through the list
|
|
When the client sends GET /v1/media/?variants=with&sort=total_size&limit=2 with a valid cookie
|
|
Then the "items" ids are 1, 4 and "next_cursor" is a non-empty string
|
|
When the client sends the same query with limit=2 and that cursor
|
|
Then the "items" ids are 2 and "next_cursor" is null
|
|
|
|
Scenario: variants=with on a library with an empty cache answers an empty list
|
|
Given no variant file exists
|
|
When the client sends GET /v1/media/?variants=with with a valid cookie
|
|
Then the response status is 200, "items" is an empty array and "counts" is unchanged
|
|
|
|
Scenario Outline: A malformed query answers 400
|
|
When the client sends GET /v1/media/?<query> with a valid cookie
|
|
Then the response status is 400 and the response body is the text apperr.MediaQueryErrorMessage
|
|
|
|
Examples:
|
|
| query |
|
|
| variants=bogus |
|
|
| variants=all |
|
|
| variants=With |
|
|
| variants= |
|
|
| sort=Total_size |
|
|
| sort=total |
|
|
|
|
Scenario: The number shown by a delete is the number a delete removes
|
|
Given image 1 has four index rows and four blobs
|
|
When the client sends DELETE /v1/media/1 with a valid cookie
|
|
Then the response status is 204 and exactly those four blobs and four rows are gone
|
|
|
|
Scenario: An article that uses a variant url still counts as using the image
|
|
Given article 1's body contains "/static/uploadedImages/<image 1 stored name>?v=hero"
|
|
When the client sends GET /v1/media/1/usage
|
|
Then article 1 is listed and the list item has used_in 1
|
|
|
|
Scenario: Merging keeps the variant parameter of the switched url
|
|
Given article 1's body contains the url of image 3 followed by "?v=hero"
|
|
When the client merges image 3 into image 2
|
|
Then article 1's body contains the url of image 2 followed by "?v=hero"
|
|
|
|
Rule: Generate all and purge by image are recorded in the activity log
|
|
|
|
Background:
|
|
Given recording follows features/activity-log-producers.feature: once, from the use case, after the change, with the request origin
|
|
And the category is "media" and the target is {"type": "image", "id": "<image id>", "name": "<filename>"}
|
|
And image 1 "north-wall.jpg" has five enabled variants and none generated
|
|
|
|
Scenario: Generate all is recorded with the counts
|
|
When the owner sends POST /v1/media/1/variants
|
|
Then one event exists with action "media.variant_generate_all", target {"type": "image", "id": "1", "name": "north-wall.jpg"}, outcome "success" and params {"generated": 5, "failed": 0, "causes": []}
|
|
|
|
Scenario: A partial failure is a success that names its causes
|
|
Given generating "hero" fails with ErrSourceTooLarge
|
|
When the owner sends POST /v1/media/1/variants
|
|
Then one event exists with action "media.variant_generate_all", outcome "success" and params {"generated": 4, "failed": 1, "causes": ["too_large"]}
|
|
|
|
Scenario: Every variant failing is a failed event with a plain reason
|
|
Given generating every variant fails with ErrSourceTooLarge
|
|
When the owner sends POST /v1/media/1/variants
|
|
Then the response status is 200 with "generated" 0 and "failed" 5
|
|
And one event exists with action "media.variant_generate_all", outcome "failed" and failure_reason "The source image is too large to generate this variant."
|
|
|
|
Scenario Outline: Nothing was done, nothing is recorded
|
|
When <request>
|
|
Then no event is recorded
|
|
|
|
Examples:
|
|
| request |
|
|
| the owner sends POST /v1/media/1/variants and all five are already generated |
|
|
| the owner sends POST /v1/media/1/variants and no variant is enabled |
|
|
| the owner sends POST /v1/media/1/variants on an SVG |
|
|
| the owner sends POST /v1/media/1/variants/thumb |
|
|
| the owner sends POST /v1/media/9999/variants |
|
|
| the owner sends GET /v1/media/1/variants |
|
|
| a public GET /static/uploadedImages/a1b2.jpg?v=thumb that generates a file |
|
|
| an upload that generates files on upload |
|
|
|
|
Scenario: Purging the files of one variant of an image is recorded
|
|
Given "thumb" is generated for image 1 with a file of 18432 bytes
|
|
When the owner sends DELETE /v1/media/1/variants/thumb
|
|
Then one event exists with action "media.variant_purge_image", target {"type": "image", "id": "1", "name": "north-wall.jpg"}, outcome "success" and params {"purged_files": 1, "purged_bytes": 18432}
|
|
|
|
Scenario Outline: A purge that removes nothing, or finds nothing, is not recorded
|
|
When <request>
|
|
Then no event is recorded
|
|
|
|
Examples:
|
|
| request |
|
|
| the owner sends DELETE /v1/media/1/variants/thumb and nothing is generated |
|
|
| the owner sends DELETE /v1/media/1/variants/missing |
|
|
| the owner sends DELETE /v1/media/9999/variants/thumb |
|
|
|
|
Scenario: A purge that fails on storage is recorded as failed
|
|
Given image.Storage fails to delete the thumb blob
|
|
When the owner sends DELETE /v1/media/1/variants/thumb
|
|
Then the response is 500 and one event exists with action "media.variant_purge_image", outcome "failed" and failure_reason "Something went wrong on our side. Please try again."
|
|
|
|
Scenario: A failing audit write never changes the response
|
|
Given the audit repository fails on Append
|
|
When the owner sends POST /v1/media/1/variants
|
|
Then the response is 200 with the same body and the files exist
|
|
|
|
Scenario: The new events read back through the existing activity API
|
|
Given one generate-all and one purge-by-image event were recorded
|
|
When the owner sends GET /v1/activity?category=media
|
|
Then both events are listed with their documented shape, params and target
|
|
|
|
Rule: Layering and hygiene
|
|
|
|
Scenario: The dependency rule holds
|
|
Then internal/domain/image imports only the stdlib and other internal/domain packages
|
|
And the new use cases (list image variants, generate image variant, generate all, purge image variant, generate on upload) import no persistence, mailer, models, gorm, net/http, chi or api package
|
|
And the media handler imports no persistence, mailer, models or gorm package
|
|
And only api/dependecyInjection.go constructs the new use cases and wires them into media.NewHandler
|
|
And no comment is added to any Go file, test or doc, except tool directives
|