Files
RenewCMS/test/article_body_unlimited_http_test.go

205 lines
7.2 KiB
Go

package test
import (
"RenewCMS/api/handlers/article"
"RenewCMS/api/middleware"
"RenewCMS/api/pkg/apperr"
validator "RenewCMS/api/pkg/validator"
"RenewCMS/internal/infrastructure/markdown"
"RenewCMS/internal/infrastructure/useCases"
"encoding/json"
"net/http"
"net/http/httptest"
"strconv"
"strings"
"testing"
"github.com/go-chi/chi/v5"
"github.com/go-chi/jwtauth/v5"
)
func newUnlimitedBodyRouter(articleRepo *mockArticleRepo, token *jwtauth.JWTAuth) *chi.Mux {
getUserUseCase := useCases.NewGetUserUseCase(newVerifiedUserRepo())
renderer := markdown.NewConverter()
articleHandler := article.NewHandler(
useCases.NewCreateArticleUseCase(articleRepo, renderer, discardRecorder, plainSites(), plainAuthors()),
useCases.NewGetArticleUseCase(articleRepo, renderer),
useCases.NewListArticlesUseCase(articleRepo, renderer, plainSites()),
useCases.NewDeleteArticleUseCase(articleRepo, discardRecorder),
useCases.NewUpdateArticleUseCase(articleRepo, renderer, discardRecorder, plainSites(), plainAuthors(), newMockImageRepo()),
getUserUseCase,
)
r := chi.NewRouter()
r.Route("/v1", func(r chi.Router) {
r.Route("/article", func(r chi.Router) {
r.Group(func(r chi.Router) {
r.Use(jwtauth.Verifier(token))
r.Group(func(r chi.Router) {
r.Use(jwtauth.Authenticator(token))
r.Use(middleware.RequireVerifiedUser(getUserUseCase))
r.Post("/", articleHandler.PostArticle)
r.Get("/{id}", articleHandler.GetArticle)
r.Patch("/{id}", articleHandler.UpdateArticleBody)
})
})
})
})
return r
}
func serveUnlimitedBody(t *testing.T, router *chi.Mux, token *jwtauth.JWTAuth, method, target, payload string) *httptest.ResponseRecorder {
t.Helper()
req := newEditArticleBodyRequest(method, target, payload)
setEditArticleBodyJwtCookie(t, req, token)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
return rec
}
func TestArticleBodyUnlimitedHttp(t *testing.T) {
validator.InitValidator()
token := jwtauth.New("HS256", []byte("test-secret"), nil)
const title = "Choosing tiles for a small bathroom"
createCases := []struct {
name string
query string
payload string
format string
rendered string
}{
{"markdown without body key", "", `{"title": "` + title + `"}`, "markdown", `""`},
{"html without body key", "?format=html", `{"title": "` + title + `"}`, "html", `""`},
{"json without body key", "?format=json", `{"title": "` + title + `"}`, "json", `[]`},
{"markdown with empty body", "", `{"title": "` + title + `", "body": ""}`, "markdown", `""`},
}
for _, tc := range createCases {
t.Run("Create with only a title succeeds in every body format: "+tc.name, func(t *testing.T) {
repo := newMockArticleRepo()
router := newUnlimitedBodyRouter(repo, token)
rec := serveUnlimitedBody(t, router, token, http.MethodPost, "/v1/article/"+tc.query, tc.payload)
if rec.Code != http.StatusCreated {
t.Fatalf("expected 201, got %d: %s", rec.Code, rec.Body.String())
}
var got struct {
Title string `json:"title"`
IsOnline bool `json:"is_online"`
BodyFormat string `json:"body_format"`
Body json.RawMessage `json:"body"`
}
if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil {
t.Fatalf("failed to decode response: %v", err)
}
if got.Title != title || got.IsOnline || got.BodyFormat != tc.format || string(got.Body) != tc.rendered {
t.Errorf("unexpected response: %s", rec.Body.String())
}
if len(repo.articles) != 1 {
t.Fatalf("expected one article, got %d", len(repo.articles))
}
for _, stored := range repo.articles {
if stored.Body != "" {
t.Errorf("expected empty stored body, got %q", stored.Body)
}
}
})
}
t.Run("A body of 120,000 characters is accepted on create", func(t *testing.T) {
repo := newMockArticleRepo()
router := newUnlimitedBodyRouter(repo, token)
body := strings.Repeat("a", 120000)
rec := serveUnlimitedBody(t, router, token, http.MethodPost, "/v1/article/", `{"title": "`+title+`", "body": "`+body+`"}`)
if rec.Code != http.StatusCreated {
t.Fatalf("expected 201, got %d", rec.Code)
}
for _, stored := range repo.articles {
if stored.Body != body {
t.Errorf("stored body differs, length %d", len(stored.Body))
}
}
})
for _, length := range []int{10001, 120000} {
t.Run("Update the body with no length limit: "+strconv.Itoa(length), func(t *testing.T) {
repo := newEditArticleBodyTestRepo()
router := newUnlimitedBodyRouter(repo, token)
body := strings.Repeat("a", length)
rec := serveUnlimitedBody(t, router, token, http.MethodPatch, "/v1/article/1", `{"body": "`+body+`"}`)
if rec.Code != http.StatusOK {
t.Fatalf("expected 200, got %d", rec.Code)
}
if len(repo.articles[1].Body) != length {
t.Errorf("expected %d characters, got %d", length, len(repo.articles[1].Body))
}
})
}
t.Run("Update the body to empty", func(t *testing.T) {
repo := newEditArticleBodyTestRepo()
router := newUnlimitedBodyRouter(repo, token)
rec := serveUnlimitedBody(t, router, token, http.MethodPatch, "/v1/article/1", `{"body": ""}`)
if rec.Code != http.StatusOK {
t.Fatalf("expected 200, got %d", rec.Code)
}
if repo.articles[1].Body != "" {
t.Errorf("expected empty body, got %q", repo.articles[1].Body)
}
get := serveUnlimitedBody(t, router, token, http.MethodGet, "/v1/article/1?format=json", "")
var got struct {
Body json.RawMessage `json:"body"`
}
if err := json.Unmarshal(get.Body.Bytes(), &got); err != nil {
t.Fatalf("failed to decode response: %v", err)
}
if string(got.Body) != "[]" {
t.Errorf("expected [], got %s", got.Body)
}
})
titleCases := []struct {
name string
payload string
}{
{"missing title", `{"body": "Hello"}`},
{"title too short", `{"title": "Ti", "body": "Hello"}`},
{"title too long", `{"title": "` + strings.Repeat("a", 201) + `"}`},
}
for _, tc := range titleCases {
t.Run("Title rules are unchanged: "+tc.name, func(t *testing.T) {
repo := newMockArticleRepo()
router := newUnlimitedBodyRouter(repo, token)
rec := serveUnlimitedBody(t, router, token, http.MethodPost, "/v1/article/", tc.payload)
if rec.Code != http.StatusBadRequest {
t.Fatalf("expected 400, got %d", rec.Code)
}
if len(repo.articles) != 0 {
t.Errorf("expected no article, got %d", len(repo.articles))
}
})
}
t.Run("Existing guards on PATCH are unchanged", func(t *testing.T) {
repo := newEditArticleBodyTestRepo()
router := newUnlimitedBodyRouter(repo, token)
rec := serveUnlimitedBody(t, router, token, http.MethodPatch, "/v1/article/abc", `{"body": "x"}`)
if rec.Code != http.StatusBadRequest || rec.Body.String() != apperr.IdUint32ErrorMessage+"\n" {
t.Errorf("unexpected id guard: %d %q", rec.Code, rec.Body.String())
}
rec = serveUnlimitedBody(t, router, token, http.MethodPatch, "/v1/article/999", `{"body": "x"}`)
if rec.Code != http.StatusNotFound || rec.Body.String() != apperr.ArticleNotFoundErrorMessage+"\n" {
t.Errorf("unexpected not found guard: %d %q", rec.Code, rec.Body.String())
}
rec = serveUnlimitedBody(t, router, token, http.MethodPatch, "/v1/article/1", `not json`)
if rec.Code != http.StatusBadRequest || rec.Body.String() != apperr.BodyErrorMessage+"\n" {
t.Errorf("unexpected malformed guard: %d %q", rec.Code, rec.Body.String())
}
})
}