Feature: The article title rule is the same in the request and the domain Superseded in part by article_edit_revision: titles are editable. Superseded in part by roles_permissions_content_scope: the article routes are gated by the content.* permissions of the caller's role (content.create, content.update, content.delete and content.publish; a refusal is 403 "missing_permission" with the header X-Missing-Permission), and content.read decides which drafts show: every draft at all, only the caller's own drafts at own, none otherwise. A draft the caller may not read answers 404 "Article not found", and a role that cannot be resolved is treated as holding no permission. As the maintainer of RenewCMS I want PostArticle's title validation and article.CreateArticle's title validation to enforce the identical rule, counted the identical way So that a title the request layer accepts can never be rejected by the domain as a surprise 500, and a domain title rejection that does happen is always reported as a 400 with a client-facing apperr message instead of a raw 500 Background: Given internal/domain/article defines the sentinel article.ErrInvalidTitle And CreateArticleUseCase.CreateArticle rejects a title whose length, counted with utf8.RuneCountInString (runes, not bytes), is below 3 or above 200, returning article.ErrInvalidTitle and storing nothing And api/handlers/article/articleHandler.go's PostArticle.Title carries validate:"required,min=3,max=200", and go-playground/validator's min/max on a string already count runes with utf8.RuneCountInString, so the request tag and the domain check now agree on both the bound (3-200) and the unit (runes) And api/pkg/apperr defines ArticleTitleErrorMessage as "Article title must be between 3 and 200 characters.", a client-facing string distinct from both the raw validator error text and "Failed to create article" And Handler.PostArticle maps errors.Is(err, article.ErrInvalidTitle) from CreateArticleUseCase.CreateArticle to a 400 response with body apperr.ArticleTitleErrorMessage; any other error from CreateArticle still answers 500 with body "Failed to create article" And POST /v1/article/ sits behind the jwtauth Verifier and Authenticator group and requires a valid jwt cookie And PATCH /v1/article/{id} accepts `title` with the same 3-200 rune rule, rejected as 400 with apperr.ArticleTitleErrorMessage (see features/article_edit_revision.feature) Scenario Outline: CreateArticle now measures title length in runes, not bytes Given an empty article repository fake When CreateArticle is called with a title made of repetitions of the 4-byte rune "😀" and a body "Some body text" Then the result is Examples: | rune_count | outcome | | 2 | ErrInvalidTitle, nothing stored | | 3 | created | | 200 | created | | 201 | ErrInvalidTitle, nothing stored | Scenario Outline: POST /v1/article/ enforces the same 3-200 rune boundary over HTTP Given a valid jwt cookie for a user with id 1 When the client sends POST /v1/article/ with that cookie, a title of ASCII characters and a body "Some body text" Then the response status is And Examples: | length | status | storage_effect | | 2 | 400 | not created | | 3 | 201 | created, is_online false | | 200 | 201 | created, is_online false | | 201 | 400 | not created | Scenario: A 200-rune multibyte title is accepted end to end over HTTP Given a valid jwt cookie for a user with id 1 When the client sends POST /v1/article/ with that cookie, a title made of 200 repetitions of the 4-byte rune "😀" (200 runes, 800 bytes) and a body "Some body text" Then the response status is 201 And the response body is the created article's JSON with that title Scenario: The handler maps a domain title rejection to 400, not 500 Given a valid jwt cookie for a user with id 1 And the article repository fake is configured so its next Create call returns article.ErrInvalidTitle directly, standing in for any layer beneath the use case's own check rejecting the title with that same sentinel When the client sends POST /v1/article/ with that cookie, title "Valid Title" and body "Some body text" (both already satisfying PostArticle's own validate tags) Then the response status is 400 And the response body is the text apperr.ArticleTitleErrorMessage And the response body is not "Failed to create article" Scenario Outline: PATCH /v1/article/{id} enforces the same 3-200 rune boundary Given a valid jwt cookie for a user holding content.update And the article repository contains an article with id 1 When the client sends PATCH /v1/article/1 with that cookie and a title of ASCII characters Then the response status is And Examples: | length | status | storage_effect | | 2 | 400 | the title is unchanged | | 3 | 200 | the title is stored | | 200 | 200 | the title is stored | | 201 | 400 | the title is unchanged | Scenario: A domain title rejection on update is a 400, not a 500 Given a valid jwt cookie for a user holding content.update And the article repository fake is configured so its next Update call returns article.ErrInvalidTitle When the client sends PATCH /v1/article/1 with that cookie and title "Valid Title" Then the response status is 400 And the response body is the text apperr.ArticleTitleErrorMessage Scenario: Editing the title does not change the slug Given a valid jwt cookie for a user holding content.update And the article with id 1 has slug "backyard-renovation-update" When the client sends PATCH /v1/article/1 with that cookie and title "A brand new title" Then the response status is 200 And the slug is still "backyard-renovation-update" Scenario: A non-title repository failure during creation still answers 500 Given a valid jwt cookie for a user with id 1 And the article repository fake is configured to fail on its next Create call with a generic repository error, not article.ErrInvalidTitle When the client sends POST /v1/article/ with that cookie, title "Valid Title" and body "Some body text" Then the response status is 500 And the response body is the text "Failed to create article"