Files
RenewCMS/features/image_variants_per_image_api.feature
T

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