133 lines
7.9 KiB
Gherkin
133 lines
7.9 KiB
Gherkin
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,
|
|
and PATCH /v1/article/{id}/unpublish 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"
|