refac: authenticated user can publish and unpublish an article; an unknown id returns 404
This commit is contained in:
1 parent
790d4e6da6
commit
8822bdddd0
9 files changed
+538
-6
No files matched your search
@@ -0,0 +1,109 @@
|
||||
Feature: An authenticated user can publish and unpublish an article
|
||||
|
||||
As an authenticated RenewCMS user managing the CMS
|
||||
I want to flip an article's is_online flag from an HTTP endpoint
|
||||
So that a draft can be published to the public site and a published article can be
|
||||
pulled back offline, without deleting it
|
||||
|
||||
Background:
|
||||
Given internal/domain/article.Repository already exposes
|
||||
UpdateIsOnline(id uint32, isOnline bool) (Article, error), and
|
||||
UpdateArticleUseCase.UpdateIsOnline delegates to it unchanged
|
||||
And UpdateArticleUseCase is already built in api/dependecyInjection.go as
|
||||
container.UpdateArticleUseCase, but before this slice no handler or route calls it
|
||||
And this slice adds Handler.PublishArticle and Handler.UnpublishArticle, backed by
|
||||
UpdateArticleUseCase.UpdateIsOnline(id, true) and UpdateIsOnline(id, false)
|
||||
And this slice mounts PATCH /v1/article/{id}/publish and
|
||||
PATCH /v1/article/{id}/unpublish inside the same jwtauth Verifier + Authenticator
|
||||
group as GET /v1/article/{id}, POST /v1/article/ and DELETE /v1/article/{id} in
|
||||
api/router.go, so both require a valid jwt cookie
|
||||
And a successful publish or unpublish answers 200 with the JSON body of the updated
|
||||
article.Article, the same shape GetArticle and PostArticle already return
|
||||
And an id that does not parse as a uint32 answers 400 with body
|
||||
apperr.IdUint32ErrorMessage, the same as GetArticle and DeleteArticle
|
||||
And an id absent from the article repository answers 404 with body
|
||||
apperr.ArticleNotFoundErrorMessage, backed by errors.Is(err, article.ErrArticleNotFound),
|
||||
the same as DeleteArticle already does per features/write_not_found_errors.feature
|
||||
And GET /v1/article/ with no jwt cookie only returns online articles, per
|
||||
features/public_article_listing.feature; this slice does not change that behavior,
|
||||
only feeds it
|
||||
|
||||
Scenario: Publishing an offline article makes it online
|
||||
Given a valid jwt cookie for a user with id 1
|
||||
And the article repository contains an offline article titled "Draft: Kitchen Remodel" with id 2
|
||||
When the client sends PATCH /v1/article/2/publish with that cookie
|
||||
Then the response status is 200
|
||||
And the response body is the article with id 2 and "is_online": true
|
||||
|
||||
Scenario: Unpublishing an online article makes it offline
|
||||
Given a valid jwt cookie for a user with id 1
|
||||
And the article repository contains an online article titled "Backyard Renovation Update" with id 1
|
||||
When the client sends PATCH /v1/article/1/unpublish with that cookie
|
||||
Then the response status is 200
|
||||
And the response body is the article with id 1 and "is_online": false
|
||||
|
||||
Scenario: Publishing an already-online article is idempotent
|
||||
Given a valid jwt cookie for a user with id 1
|
||||
And the article repository contains an online article with id 1
|
||||
When the client sends PATCH /v1/article/1/publish with that cookie
|
||||
Then the response status is 200
|
||||
And the response body is the article with id 1 and "is_online": true
|
||||
And no error is returned
|
||||
|
||||
Scenario: Unpublishing an already-offline article is idempotent
|
||||
Given a valid jwt cookie for a user with id 1
|
||||
And the article repository contains an offline article with id 2
|
||||
When the client sends PATCH /v1/article/2/unpublish with that cookie
|
||||
Then the response status is 200
|
||||
And the response body is the article with id 2 and "is_online": false
|
||||
And no error is returned
|
||||
|
||||
Scenario Outline: Publishing or unpublishing an unknown article answers 404
|
||||
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 PATCH /v1/article/9999/<action> with that cookie
|
||||
Then the response status is 404
|
||||
And the response body is the text "Article not found"
|
||||
|
||||
Examples:
|
||||
| action |
|
||||
| publish |
|
||||
| unpublish |
|
||||
|
||||
Scenario Outline: A malformed id answers 400 before any repository call
|
||||
Given a valid jwt cookie for a user with id 1
|
||||
When the client sends PATCH /v1/article/not-a-number/<action> with that cookie
|
||||
Then the response status is 400
|
||||
And the response body is the text apperr.IdUint32ErrorMessage
|
||||
|
||||
Examples:
|
||||
| action |
|
||||
| publish |
|
||||
| unpublish |
|
||||
|
||||
Scenario Outline: Publish and unpublish require authentication
|
||||
Given no jwt cookie is sent
|
||||
And the article repository contains an article with id 1
|
||||
When the client sends PATCH /v1/article/1/<action> without a cookie
|
||||
Then the response status is 401
|
||||
|
||||
Examples:
|
||||
| action |
|
||||
| publish |
|
||||
| unpublish |
|
||||
|
||||
Scenario: Publishing an article makes it appear in the public online-only list
|
||||
Given a valid jwt cookie for a user with id 1
|
||||
And the article repository contains an offline article titled "Draft: Kitchen Remodel" with id 2
|
||||
When the client sends PATCH /v1/article/2/publish with that cookie
|
||||
And the client then sends GET /v1/article/ with no jwt cookie
|
||||
Then the response status of the second request is 200
|
||||
And the response body of the second request is a JSON array containing the article with id 2
|
||||
|
||||
Scenario: Unpublishing an article removes it from the public online-only list
|
||||
Given a valid jwt cookie for a user with id 1
|
||||
And the article repository contains an online article titled "Backyard Renovation Update" with id 1
|
||||
When the client sends PATCH /v1/article/1/unpublish with that cookie
|
||||
And the client then sends GET /v1/article/ with no jwt cookie
|
||||
Then the response status of the second request is 200
|
||||
And the article with id 1 is absent from the response body of the second request
|
||||
@@ -0,0 +1,57 @@
|
||||
# QA procedure: article_publishing
|
||||
|
||||
Package: `test` (black-box, extending `article_test.go`).
|
||||
|
||||
## Setup shared by every case
|
||||
1. Call `validator.InitValidator()` once.
|
||||
2. Reuse `mockArticleRepo` (`test/article_test.go`) as-is; its `UpdateIsOnline` and `FindByFilters`
|
||||
already support everything this slice needs, no fake changes expected.
|
||||
3. Wire the real use cases over the fake: `useCases.NewCreateArticleUseCase`,
|
||||
`useCases.NewGetArticleUseCase`, `useCases.NewListArticlesUseCase`,
|
||||
`useCases.NewDeleteArticleUseCase`, `useCases.NewUpdateArticleUseCase`. Build
|
||||
`article.NewHandler(...)` extended to also take the update use case (matching whatever
|
||||
`NewHandler` signature the coder lands on), exposing the new `PublishArticle` and
|
||||
`UnpublishArticle` methods.
|
||||
4. Build a test `jwtauth.New("HS256", []byte("test-secret"), nil)`.
|
||||
5. Mount the routes on a fresh `chi.NewRouter()` mirroring `api/router.go`'s current shape
|
||||
(per `public_article_listing.qa.md`): `GET /` under `jwtauth.Verifier` only; `GET /{id}`,
|
||||
`POST /`, `DELETE /{id}` under `jwtauth.Verifier` + `jwtauth.Authenticator`. Add
|
||||
`PATCH /{id}/publish` and `PATCH /{id}/unpublish` to that same authenticated group.
|
||||
6. Mint a valid jwt cookie for user id 1: `token.Encode(map[string]any{"user_id": uint32(1)})`,
|
||||
set as the `jwt` cookie on the relevant `httptest.NewRequest`.
|
||||
7. Seed the repo directly via the map pattern already used elsewhere, e.g.
|
||||
`repo.articles[1] = article.Article{ID: 1, Title: "Backyard Renovation Update", IsOnline: true}`
|
||||
and `repo.articles[2] = article.Article{ID: 2, Title: "Draft: Kitchen Remodel", IsOnline: false}`.
|
||||
|
||||
## HTTP checks
|
||||
8. `PATCH /v1/article/2/publish` with the valid cookie, repo seeded per step 7 -> status `200`;
|
||||
decode the JSON body and assert `id == 2` and `is_online == true`; assert `repo.articles[2].IsOnline`
|
||||
is now `true`.
|
||||
9. `PATCH /v1/article/1/unpublish` with the valid cookie, repo seeded per step 7 -> status `200`;
|
||||
decode the JSON body and assert `id == 1` and `is_online == false`; assert
|
||||
`repo.articles[1].IsOnline` is now `false`.
|
||||
10. `PATCH /v1/article/1/publish` with the valid cookie against an article already seeded
|
||||
`IsOnline: true` -> status `200`, body `is_online == true`, no error (idempotency).
|
||||
11. `PATCH /v1/article/2/unpublish` with the valid cookie against an article already seeded
|
||||
`IsOnline: false` -> status `200`, body `is_online == false`, no error (idempotency).
|
||||
12. `PATCH /v1/article/9999/publish` with the valid cookie, empty repo id `9999` -> status `404`,
|
||||
body `"Article not found\n"`. Repeat for `PATCH /v1/article/9999/unpublish` -> same assertions.
|
||||
13. `PATCH /v1/article/not-a-number/publish` with the valid cookie -> status `400`, body matching
|
||||
`apperr.IdUint32ErrorMessage + "\n"`. Repeat for `.../unpublish`.
|
||||
14. `PATCH /v1/article/1/publish` with **no** cookie, repo seeded with article id 1 -> status `401`.
|
||||
Repeat for `PATCH /v1/article/1/unpublish` with no cookie -> status `401`.
|
||||
15. Integration with the public listing: seed repo with an offline article id 2, call
|
||||
`PATCH /v1/article/2/publish` with the valid cookie (assert `200`), then call
|
||||
`GET /v1/article/` with no cookie and assert the decoded JSON array contains an element with
|
||||
id `2`.
|
||||
16. Mirror check: seed repo with an online article id 1, call `PATCH /v1/article/1/unpublish` with
|
||||
the valid cookie (assert `200`), then call `GET /v1/article/` with no cookie and assert the
|
||||
decoded JSON array does not contain an element with id `1`.
|
||||
|
||||
## Manual verification (not exercised by the Go test)
|
||||
- SPA-visible behaviour: confirm the back-office article editor exposes a publish/unpublish
|
||||
control that calls the new endpoints and updates the visible online/offline state, against the
|
||||
real embedded frontend and a real SQLite file. Not exercised by `task test`; requires `task dev`
|
||||
or a built `task build` binary. (No such UI exists yet as of this slice — this note applies once
|
||||
the SPA is wired to the new endpoints.)
|
||||
- No SMTP involvement.
|
||||
Reference in new issue
Block a user