feat(API): media library

This commit is contained in:
fsylvain committed 2026-09-30 18:35:28 +02:00
1 parent 95ae043d3e
commit 0d99544579
65 files changed
+3992 -1530

No files matched your search

+38 -90
View File
@@ -1,99 +1,47 @@
Feature: Uploading an image to an article
Feature: POST /v1/article/{id}/image is a thin alias that creates a library image
As an authenticated RenewCMS user managing the CMS
I want to attach an image file to an existing article and get back the
image's public location
So that an unknown article answers 404 the same way every other article
write does, without ever creating an orphaned image, and a disallowed
content type is rejected with 400 before it becomes part of the
article, without a maintainer needing to trust the client to send a
sane file
As the current editor, until the editor slice moves it to /v1/media
I want to keep uploading through the article route and get {id, location} back
So that the editor keeps working while images are library assets
Background:
Given internal/infrastructure/useCases.CreateImageUseCase.CreateImage
already checks the uploaded content type against the allowed list
(image/png, image/jpeg, image/webp, image/svg+xml) and the size
against the 10485760-byte maximum before calling image.Repository.Create,
returning image.ErrImageTypeNotAllowed or image.ErrImageTooLarge and
calling the repository not at all when either check fails
And internal/infrastructure/persistence's ArticleRepository.AddImage
already returns article.ErrArticleNotFound when the article id doesn't
match any row, and image.ErrImageNotFound when the article exists but
the image id doesn't, both asserted with errors.Is
And before this slice, api/handlers/image.Handler.PostImage existed and
was wired to CreateImageUseCase and UpdateArticleUseCase.AddImage, but
no route called it, and every error it got back from either use case
fell through to a blanket 500
And this slice mounts POST /v1/article/{id}/image in api/router.go, in
the same jwtauth Verifier + Authenticator group as the other article
write routes, and changes the upload flow so that the article's
existence is confirmed before the image is created, so that an
unknown article id never leaves an orphaned image behind
And this slice changes PostImage's error handling so that errors.Is(err,
image.ErrImageTypeNotAllowed) or errors.Is(err, image.ErrImageTooLarge)
answers 400 with the sentinel's own message, errors.Is(err,
article.ErrArticleNotFound) answers 404 with body
apperr.ArticleNotFoundErrorMessage, and any other error still falls
back to a generic 500
And PostImage reads the uploaded file from the multipart form field
"file"
And on success PostImage answers 201 with a JSON body
{"id": ..., "location": ...} holding the created image's numeric id
and public path, e.g. "/static/uploadedImages/<stored-filename>"
And this slice (features/remove_article_image.feature) adds the "id"
field to that body; the id lets a client later address the image
through DELETE /v1/article/{id}/image/{imageId}
Given the route stays in the article router's jwtauth group with RequireCurrentToken and
RequireVerifiedUser, and is marked for removal in the editor slice
And the handler calls GetArticleUseCase first, so an unknown article is 404
apperr.ArticleNotFoundErrorMessage and nothing is created
And it then calls the same CreateImageUseCase as POST /v1/media/, with UploadedByID from the
jwt user_id claim, attaches nothing to the article, and answers 201 {"id", "location"}
where location is the image's url
And type, size and unreadable errors map as on /v1/media/: 415, 413, 400
And DELETE /v1/article/{id}/image/{imageId} is not mounted
Scenario: Uploading an allowed image type to an existing article succeeds
Given a valid jwt cookie for a user with id 1
And the article repository contains an article with id 1 titled
"Backyard Renovation Update"
When the client sends POST /v1/article/1/image with that cookie and a
multipart form field "file" holding a file named "before.png" with
content type "image/png"
Scenario: Uploading through the alias creates a library image
Given a valid jwt cookie for user 1 and article 1 "Backyard Renovation Update"
When the client sends POST /v1/article/1/image with a real PNG named "before.png"
Then the response status is 201
And the response body is JSON with a "location" field whose value is
"/static/uploadedImages/before.png"
And the response body's "id" field equals the new image's id in the
image repository
And the image repository contains a new image whose Path is
"/static/uploadedImages/before.png"
And the article repository's article with id 1 has that new image's id
in its Images slice
And the body has "id" equal to the new image's id and "location" equal to its url
And GET /v1/media/ lists the image with uploaded_by of user 1
And article 1 is unchanged
Scenario: Uploading to an unknown article answers 404 and creates no image
Given a valid jwt cookie for a user with id 1
And the article repository has no article with id 9999
When the client sends POST /v1/article/9999/image with that cookie and a
multipart form field "file" holding a file named "before.png" with
content type "image/png"
Then the response status is 404
And the response body is the text "Article not found"
Scenario: An unknown article is 404 and creates no image
When the client sends POST /v1/article/9999/image with a valid PNG
Then the response status is 404 and the body is "Article not found"
And the image repository's Create is never called
And no image exists in the image repository
Scenario: Uploading a disallowed content type answers 400 before the article is touched
Given a valid jwt cookie for a user with id 1
And the article repository contains an article with id 1 titled
"Backyard Renovation Update"
When the client sends POST /v1/article/1/image with that cookie and a
multipart form field "file" holding a file named "notes.pdf" with
content type "application/pdf"
Then the response status is 400
And the response body is the text
"the file must be a PNG, JPEG, WEBP, or SVG image"
And the image repository's Create is never called
And the article repository's article with id 1 still has an empty
Images slice
Scenario Outline: Rejected files use the media status codes
When the client sends POST /v1/article/1/image with <file>
Then the response status is <status> and Create is never called
Scenario: Uploading an image still requires authentication
Given no jwt cookie is sent
And the article repository contains an article with id 1 titled
"Backyard Renovation Update"
When the client sends POST /v1/article/1/image without a cookie and a
multipart form field "file" holding a file named "before.png" with
content type "image/png"
Then the response status is 401
And the image repository's Create is never called
And the article repository's article with id 1 still has an empty
Images slice
Examples:
| file | status |
| "notes.pdf" as application/pdf | 415 |
| a valid PNG padded to 10485761 bytes | 413 |
| "broken.png" as image/png with text bytes | 400 |
Scenario: The alias still requires authentication
When the client sends POST /v1/article/1/image with no cookie
Then the response status is 401 and Create is never called
Scenario: The removed delete route is gone
When the client sends DELETE /v1/article/1/image/10 with a valid cookie
Then the response status is 405 and image 10 still exists