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 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"