fix: reading a single article follows the same rule as the list: anyone can read an online article, a verified user can also read an offline one, and an unknown or hidden id returns 404
This commit is contained in:
1 parent
2ee28203ab
commit
81c4b62175
11 files changed
+510
-28
No files matched your search
@@ -0,0 +1,132 @@
|
||||
Feature: Single article read follows the same online/offline visibility rule as the list
|
||||
|
||||
As a visitor of the public site and as an authenticated RenewCMS user
|
||||
I want GET /v1/article/{id} to return an online article to anyone, and an offline article
|
||||
only to a caller resolved as verified, with an unknown id and a hidden offline id
|
||||
answering the exact same 404
|
||||
So that the public site can read a single published article through the API, and a verified
|
||||
user can open their own unpublished draft to edit it, while a draft never leaks its existence
|
||||
to anyone else
|
||||
|
||||
Background:
|
||||
Given internal/domain/article.Article has a json "is_online" field and the sentinel
|
||||
article.ErrArticleNotFound
|
||||
And GetArticleUseCase.GetArticle(id) returns the article regardless of is_online, and
|
||||
GetArticleUseCase.GetOnlineArticle(id) returns article.ErrArticleNotFound when the article
|
||||
exists but is offline, both already implemented and unchanged by this slice
|
||||
And before this slice, GET /v1/article/{id} sits inside the jwtauth Verifier + Authenticator +
|
||||
middleware.RequireVerifiedUser group in api/router.go, so a missing jwt cookie answers 401, an
|
||||
authenticated-but-unverified caller answers 403, and even a verified caller always resolves
|
||||
through GetOnlineArticle, so an offline article answers 404 no matter who asks
|
||||
(features/not_found_errors.feature, features/verified_account_required.feature)
|
||||
And this slice moves GET /v1/article/{id} out of that group and into the jwtauth.Verifier-only
|
||||
group that already holds GET /v1/article/ (features/public_article_listing.feature), so a
|
||||
missing or invalid jwt cookie no longer blocks the request; POST /v1/article/,
|
||||
DELETE /v1/article/{id}, PATCH /v1/article/{id}, PATCH /v1/article/{id}/publish,
|
||||
PATCH /v1/article/{id}/unpublish and POST /v1/article/{id}/image keep requiring
|
||||
Verifier + Authenticator + RequireVerifiedUser, unchanged
|
||||
And Handler.GetArticle reuses the existing Handler.callerIsVerified(r) helper already used by
|
||||
Handler.ListArticles: when the request resolves to a verified account it calls
|
||||
GetUseCase.GetArticle(id); otherwise (no jwt, malformed jwt, unresolvable user_id, a deleted
|
||||
account, or an account that exists but is not yet verified) it calls
|
||||
GetUseCase.GetOnlineArticle(id), exactly the same fallback rule
|
||||
features/verified_account_required.feature already documents for the listing endpoint
|
||||
And an id that does not parse as a uint32 still answers 400 with apperr.IdUint32ErrorMessage
|
||||
before any lookup happens, for every caller, unchanged
|
||||
And a domain article.ErrArticleNotFound, whether the id is truly unknown or is an offline
|
||||
article hidden from this caller, still answers 404 with apperr.ArticleNotFoundErrorMessage
|
||||
("Article not found"), the identical body either way, so no response distinguishes the two
|
||||
And a repository failure other than article.ErrArticleNotFound still answers 500 with the text
|
||||
"Internal server error", regardless of caller state, unchanged
|
||||
And this slice amends features/verified_account_required.feature: the Examples table of
|
||||
"An authenticated but unverified user is refused on every protected article route" and of "A
|
||||
request with no jwt cookie is still refused with 401, never 403" both drop their
|
||||
"GET /v1/article/1" row, since that route no longer applies RequireVerifiedUser or requires a
|
||||
cookie at all
|
||||
And this slice amends features/not_found_errors.feature: "Fetching an offline article still
|
||||
answers 404 with the same message" now holds only for an anonymous or unverified caller, not
|
||||
a verified one, and "The article route still requires authentication before existence is even
|
||||
checked" is replaced by this feature's public-visibility scenarios below
|
||||
|
||||
Scenario: An anonymous visitor can read an online article
|
||||
Given the article repository contains an online article titled "Backyard Renovation Update" with id 1
|
||||
When the client sends GET /v1/article/1 with no jwt cookie
|
||||
Then the response status is 200
|
||||
And the response body is the article's JSON representation
|
||||
|
||||
Scenario: An anonymous visitor is told an offline article does not exist
|
||||
Given the article repository contains an offline article titled "Draft: Kitchen Remodel" with id 2
|
||||
When the client sends GET /v1/article/2 with no jwt cookie
|
||||
Then the response status is 404
|
||||
And the response body is the text "Article not found"
|
||||
|
||||
Scenario: An anonymous visitor is told an unknown id does not exist
|
||||
Given the article repository has no article with id 9999
|
||||
When the client sends GET /v1/article/9999 with no jwt cookie
|
||||
Then the response status is 404
|
||||
And the response body is the text "Article not found"
|
||||
|
||||
Scenario: A verified user can open their own offline draft
|
||||
Given a user repository containing user id 1, is_verified true
|
||||
And a valid jwt cookie for user id 1
|
||||
And the article repository contains an offline article titled "Draft: Kitchen Remodel" with id 2
|
||||
When the client sends GET /v1/article/2 with that cookie
|
||||
Then the response status is 200
|
||||
And the response body is the article's JSON representation
|
||||
|
||||
Scenario: A verified user still reads an online article normally
|
||||
Given a user repository containing user id 1, is_verified true
|
||||
And a valid jwt cookie for user id 1
|
||||
And the article repository contains an online article titled "Backyard Renovation Update" with id 1
|
||||
When the client sends GET /v1/article/1 with that cookie
|
||||
Then the response status is 200
|
||||
And the response body is the article's JSON representation
|
||||
|
||||
Scenario: A verified user requesting an unknown id gets the same not-found response
|
||||
Given a user repository containing user id 1, is_verified true
|
||||
And a valid jwt cookie for user id 1
|
||||
And the article repository has no article with id 9999
|
||||
When the client sends GET /v1/article/9999 with that cookie
|
||||
Then the response status is 404
|
||||
And the response body is the text "Article not found"
|
||||
|
||||
Scenario: An authenticated but unverified user is told an offline article does not exist, not refused with 403
|
||||
Given a user repository containing user id 1, is_verified false
|
||||
And a valid jwt cookie for user id 1
|
||||
And the article repository contains an offline article titled "Draft: Kitchen Remodel" with id 2
|
||||
When the client sends GET /v1/article/2 with that cookie
|
||||
Then the response status is 404
|
||||
And the response body is the text "Article not found"
|
||||
|
||||
Scenario: A malformed or expired jwt cookie falls back to the public online-only visibility
|
||||
Given a jwt cookie whose value is not a token issued by JWT_SECRET
|
||||
And the article repository contains an online article with id 1 and an offline article with id 2
|
||||
When the client sends GET /v1/article/1 with that cookie
|
||||
Then the response status is 200
|
||||
When the client sends GET /v1/article/2 with that cookie
|
||||
Then the response status is 404
|
||||
And the response body is the text "Article not found"
|
||||
|
||||
Scenario: An id that isn't a valid uint32 still answers 400, regardless of caller
|
||||
When the client sends GET /v1/article/not-a-number with no jwt cookie
|
||||
Then the response status is 400
|
||||
And the response body is the text apperr.IdUint32ErrorMessage
|
||||
|
||||
Scenario Outline: The rest of the article routes still require a verified account
|
||||
Given no jwt cookie is sent
|
||||
When the client sends <method> <path>
|
||||
Then the response status is 401
|
||||
|
||||
Examples:
|
||||
| method | path |
|
||||
| POST | /v1/article/ |
|
||||
| DELETE | /v1/article/1 |
|
||||
| PATCH | /v1/article/1 |
|
||||
| PATCH | /v1/article/1/publish |
|
||||
| PATCH | /v1/article/1/unpublish |
|
||||
|
||||
Scenario: A repository failure still answers 500 regardless of caller state
|
||||
Given the article repository fails on its next call to Get
|
||||
When the client sends GET /v1/article/1 with no jwt cookie
|
||||
Then the response status is 500
|
||||
And the response body is the text "Internal server error"
|
||||
Reference in new issue
Block a user