feat: articles format option with markdown new default
This commit is contained in:
1 parent
68401d66e0
commit
7e9bd9ba30
61 files changed
+2269
-243
No files matched your search
@@ -0,0 +1,361 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"RenewCMS/api/handlers/article"
|
||||
"RenewCMS/api/middleware"
|
||||
"RenewCMS/api/pkg/apperr"
|
||||
validator "RenewCMS/api/pkg/validator"
|
||||
domainArticle "RenewCMS/internal/domain/article"
|
||||
"RenewCMS/internal/infrastructure/markdown"
|
||||
"RenewCMS/internal/infrastructure/useCases"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/jwtauth/v5"
|
||||
)
|
||||
|
||||
type fakeBodyRenderer struct {
|
||||
fail bool
|
||||
calls int
|
||||
}
|
||||
|
||||
var _ domainArticle.BodyRenderer = &fakeBodyRenderer{}
|
||||
|
||||
func (f *fakeBodyRenderer) Render(html string, format domainArticle.BodyFormat) (string, error) {
|
||||
f.calls++
|
||||
if f.fail {
|
||||
return "", errors.New("render failed")
|
||||
}
|
||||
return html, nil
|
||||
}
|
||||
|
||||
const (
|
||||
bodyFormatArticleOneHTML = "<h2>Plan</h2><p>Replace the <strong>old</strong> deck.</p>"
|
||||
bodyFormatArticleOneMD = "## Plan\n\nReplace the **old** deck."
|
||||
bodyFormatArticleOneJSON = `[{"type":"heading","level":2,"children":[{"type":"text","text":"Plan","marks":[]}]},{"type":"paragraph","children":[{"type":"text","text":"Replace the ","marks":[]},{"type":"text","text":"old","marks":[{"type":"bold"}]},{"type":"text","text":" deck.","marks":[]}]}]`
|
||||
)
|
||||
|
||||
func newBodyFormatRouter(articleRepo *mockArticleRepo, userRepo *mockUserRepo, renderer domainArticle.BodyRenderer, token *jwtauth.JWTAuth) *chi.Mux {
|
||||
getUserUseCase := useCases.NewGetUserUseCase(userRepo)
|
||||
articleHandler := article.NewHandler(
|
||||
useCases.NewCreateArticleUseCase(articleRepo, renderer),
|
||||
useCases.NewGetArticleUseCase(articleRepo, renderer),
|
||||
useCases.NewListArticlesUseCase(articleRepo, renderer),
|
||||
useCases.NewDeleteArticleUseCase(articleRepo),
|
||||
useCases.NewUpdateArticleUseCase(articleRepo, renderer),
|
||||
getUserUseCase,
|
||||
)
|
||||
|
||||
r := chi.NewRouter()
|
||||
r.Route("/v1", func(r chi.Router) {
|
||||
r.Route("/article", func(r chi.Router) {
|
||||
r.Use(jwtauth.Verifier(token))
|
||||
r.Get("/", articleHandler.ListArticles)
|
||||
r.Get("/{id}", articleHandler.GetArticle)
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(jwtauth.Authenticator(token))
|
||||
r.Use(middleware.RequireVerifiedUser(getUserUseCase))
|
||||
r.Post("/", articleHandler.PostArticle)
|
||||
r.Patch("/{id}", articleHandler.UpdateArticleBody)
|
||||
r.Patch("/{id}/publish", articleHandler.PublishArticle)
|
||||
r.Patch("/{id}/unpublish", articleHandler.UnpublishArticle)
|
||||
})
|
||||
})
|
||||
})
|
||||
return r
|
||||
}
|
||||
|
||||
func seedBodyFormatRepo() *mockArticleRepo {
|
||||
repo := newMockArticleRepo()
|
||||
repo.articles[1] = domainArticle.Article{ID: 1, Title: "Backyard Renovation Update", Body: bodyFormatArticleOneHTML, IsOnline: true}
|
||||
repo.articles[2] = domainArticle.Article{ID: 2, Title: "Draft: Kitchen Remodel", Body: "<p>Draft</p>", IsOnline: false}
|
||||
repo.articles[3] = domainArticle.Article{ID: 3, Title: "Second article", Body: "<p>Second</p>", IsOnline: true}
|
||||
repo.nextID = 3
|
||||
return repo
|
||||
}
|
||||
|
||||
type bodyFormatResponse struct {
|
||||
ID uint32 `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Body json.RawMessage `json:"body"`
|
||||
BodyFormat string `json:"body_format"`
|
||||
IsOnline bool `json:"is_online"`
|
||||
}
|
||||
|
||||
func serveBodyFormat(router http.Handler, method, path, body string, cookie *http.Cookie) *httptest.ResponseRecorder {
|
||||
req := httptest.NewRequest(method, path, bytes.NewBufferString(body))
|
||||
if cookie != nil {
|
||||
req.AddCookie(cookie)
|
||||
}
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
return rec
|
||||
}
|
||||
|
||||
func decodeBodyFormatArticle(t *testing.T, rec *httptest.ResponseRecorder, expectedStatus int) bodyFormatResponse {
|
||||
t.Helper()
|
||||
if rec.Code != expectedStatus {
|
||||
t.Fatalf("expected status %d, got %d, body %q", expectedStatus, rec.Code, rec.Body.String())
|
||||
}
|
||||
var got bodyFormatResponse
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil {
|
||||
t.Fatalf("failed to decode response: %v", err)
|
||||
}
|
||||
return got
|
||||
}
|
||||
|
||||
func assertBodyFormat(t *testing.T, got bodyFormatResponse, format, expectedBody string) {
|
||||
t.Helper()
|
||||
if got.BodyFormat != format {
|
||||
t.Errorf("expected body_format %q, got %q", format, got.BodyFormat)
|
||||
}
|
||||
if format == "json" {
|
||||
assertSameJSON(t, string(got.Body), expectedBody)
|
||||
return
|
||||
}
|
||||
var text string
|
||||
if err := json.Unmarshal(got.Body, &text); err != nil {
|
||||
t.Fatalf("body is not a JSON string: %s", got.Body)
|
||||
}
|
||||
if text != expectedBody {
|
||||
t.Errorf("expected body %q, got %q", expectedBody, text)
|
||||
}
|
||||
}
|
||||
|
||||
func assertSameJSON(t *testing.T, actual, expected string) {
|
||||
t.Helper()
|
||||
var actualValue, expectedValue any
|
||||
if err := json.Unmarshal([]byte(actual), &actualValue); err != nil {
|
||||
t.Fatalf("actual is not JSON: %s", actual)
|
||||
}
|
||||
if err := json.Unmarshal([]byte(expected), &expectedValue); err != nil {
|
||||
t.Fatalf("expected is not JSON: %s", expected)
|
||||
}
|
||||
actualBytes, _ := json.Marshal(actualValue)
|
||||
expectedBytes, _ := json.Marshal(expectedValue)
|
||||
if string(actualBytes) != string(expectedBytes) {
|
||||
t.Errorf("expected %s, got %s", expectedBytes, actualBytes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestArticleBodyFormatHttp(t *testing.T) {
|
||||
validator.InitValidator()
|
||||
token := jwtauth.New("HS256", []byte("test-secret"), nil)
|
||||
newRouter := func(repo *mockArticleRepo, users *mockUserRepo, renderer domainArticle.BodyRenderer) *chi.Mux {
|
||||
return newBodyFormatRouter(repo, users, renderer, token)
|
||||
}
|
||||
realRouter := func() (*chi.Mux, *mockArticleRepo) {
|
||||
repo := seedBodyFormatRepo()
|
||||
return newRouter(repo, newPublicArticleReadUserRepo(true), markdown.NewConverter()), repo
|
||||
}
|
||||
verifiedCookie := mintPublicArticleReadCookie(t, token, 1)
|
||||
|
||||
t.Run("Single read negotiates the body format", func(t *testing.T) {
|
||||
examples := []struct {
|
||||
query, format, body string
|
||||
}{
|
||||
{"", "markdown", bodyFormatArticleOneMD},
|
||||
{"?format=", "markdown", bodyFormatArticleOneMD},
|
||||
{"?format=markdown", "markdown", bodyFormatArticleOneMD},
|
||||
{"?format=MarkDown", "markdown", bodyFormatArticleOneMD},
|
||||
{"?format=html", "html", bodyFormatArticleOneHTML},
|
||||
{"?format=HTML", "html", bodyFormatArticleOneHTML},
|
||||
{"?format=json", "json", bodyFormatArticleOneJSON},
|
||||
{"?format=JSON", "json", bodyFormatArticleOneJSON},
|
||||
}
|
||||
for _, example := range examples {
|
||||
router, _ := realRouter()
|
||||
rec := serveBodyFormat(router, http.MethodGet, "/v1/article/1"+example.query, "", nil)
|
||||
got := decodeBodyFormatArticle(t, rec, http.StatusOK)
|
||||
assertBodyFormat(t, got, example.format, example.body)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("List read negotiates every element", func(t *testing.T) {
|
||||
router, _ := realRouter()
|
||||
expectations := []struct {
|
||||
query, format, secondBody string
|
||||
}{
|
||||
{"", "markdown", "Second"},
|
||||
{"?format=html", "html", "<p>Second</p>"},
|
||||
{"?format=json", "json", `[{"type":"paragraph","children":[{"type":"text","text":"Second","marks":[]}]}]`},
|
||||
}
|
||||
for _, expectation := range expectations {
|
||||
rec := serveBodyFormat(router, http.MethodGet, "/v1/article/"+expectation.query, "", nil)
|
||||
var items []bodyFormatResponse
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &items); err != nil {
|
||||
t.Fatalf("failed to decode list: %v", err)
|
||||
}
|
||||
if len(items) != 2 {
|
||||
t.Fatalf("expected 2 online articles, got %d", len(items))
|
||||
}
|
||||
for _, item := range items {
|
||||
if item.BodyFormat != expectation.format {
|
||||
t.Errorf("expected body_format %q, got %q", expectation.format, item.BodyFormat)
|
||||
}
|
||||
if item.ID == 3 {
|
||||
assertBodyFormat(t, item, expectation.format, expectation.secondBody)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("An unsupported format is rejected", func(t *testing.T) {
|
||||
for _, path := range []string{"/v1/article/1?format=pdf", "/v1/article/?format=xml", "/v1/article/9999?format=pdf", "/v1/article/1?format=md"} {
|
||||
renderer := &fakeBodyRenderer{}
|
||||
router := newRouter(seedBodyFormatRepo(), newMockUserRepo(), renderer)
|
||||
rec := serveBodyFormat(router, http.MethodGet, path, "", nil)
|
||||
if rec.Code != http.StatusBadRequest {
|
||||
t.Fatalf("%s: expected status 400, got %d", path, rec.Code)
|
||||
}
|
||||
if rec.Body.String() != apperr.ArticleBodyFormatErrorMessage+"\n" {
|
||||
t.Errorf("%s: unexpected body %q", path, rec.Body.String())
|
||||
}
|
||||
if renderer.calls != 0 {
|
||||
t.Errorf("%s: renderer must not be called", path)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("An invalid id is reported before an invalid format", func(t *testing.T) {
|
||||
router, _ := realRouter()
|
||||
rec := serveBodyFormat(router, http.MethodGet, "/v1/article/abc?format=pdf", "", nil)
|
||||
if rec.Code != http.StatusBadRequest || rec.Body.String() != apperr.IdUint32ErrorMessage+"\n" {
|
||||
t.Errorf("unexpected response %d %q", rec.Code, rec.Body.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("The first repeated format parameter wins", func(t *testing.T) {
|
||||
router, _ := realRouter()
|
||||
rec := serveBodyFormat(router, http.MethodGet, "/v1/article/1?format=html&format=json", "", nil)
|
||||
got := decodeBodyFormatArticle(t, rec, http.StatusOK)
|
||||
assertBodyFormat(t, got, "html", bodyFormatArticleOneHTML)
|
||||
})
|
||||
|
||||
t.Run("Offline articles stay hidden whatever the format", func(t *testing.T) {
|
||||
unverified := newRouter(seedBodyFormatRepo(), newMockUserRepo(), markdown.NewConverter())
|
||||
rec := serveBodyFormat(unverified, http.MethodGet, "/v1/article/2?format=json", "", nil)
|
||||
if rec.Code != http.StatusNotFound || rec.Body.String() != apperr.ArticleNotFoundErrorMessage+"\n" {
|
||||
t.Errorf("unexpected response %d %q", rec.Code, rec.Body.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Authenticated routes negotiate the response body", func(t *testing.T) {
|
||||
paragraph := func(text string) string {
|
||||
return `[{"type":"paragraph","children":[{"type":"text","text":"` + text + `","marks":[]}]}]`
|
||||
}
|
||||
type expectedBodies map[string]string
|
||||
bodiesA := expectedBodies{"markdown": "A", "html": "<p>A</p>", "json": paragraph("A")}
|
||||
bodiesB := expectedBodies{"markdown": "B", "html": "<p>B</p>", "json": paragraph("B")}
|
||||
bodiesOne := expectedBodies{"markdown": bodyFormatArticleOneMD, "html": bodyFormatArticleOneHTML, "json": bodyFormatArticleOneJSON}
|
||||
examples := []struct {
|
||||
method, path, body string
|
||||
status int
|
||||
format string
|
||||
bodies expectedBodies
|
||||
}{
|
||||
{http.MethodPost, "/v1/article/", `{"title":"Deck","body":"<p>A</p>"}`, 201, "markdown", bodiesA},
|
||||
{http.MethodPost, "/v1/article/?format=html", `{"title":"Deck","body":"<p>A</p>"}`, 201, "html", bodiesA},
|
||||
{http.MethodPost, "/v1/article/?format=json", `{"title":"Deck","body":"<p>A</p>"}`, 201, "json", bodiesA},
|
||||
{http.MethodPatch, "/v1/article/1", `{"body":"<p>B</p>"}`, 200, "markdown", bodiesB},
|
||||
{http.MethodPatch, "/v1/article/1?format=json", `{"body":"<p>B</p>"}`, 200, "json", bodiesB},
|
||||
{http.MethodPatch, "/v1/article/1/publish", "", 200, "markdown", bodiesOne},
|
||||
{http.MethodPatch, "/v1/article/1/unpublish?format=html", "", 200, "html", bodiesOne},
|
||||
}
|
||||
for _, example := range examples {
|
||||
router, repo := realRouter()
|
||||
rec := serveBodyFormat(router, example.method, example.path, example.body, verifiedCookie)
|
||||
got := decodeBodyFormatArticle(t, rec, example.status)
|
||||
assertBodyFormat(t, got, example.format, example.bodies[example.format])
|
||||
stored := repo.articles[got.ID].Body
|
||||
if example.bodies["html"] != stored {
|
||||
t.Errorf("%s %s: stored body must stay HTML %q, got %q", example.method, example.path, example.bodies["html"], stored)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("An unsupported format on a write route changes nothing", func(t *testing.T) {
|
||||
requests := []struct {
|
||||
method, path, body string
|
||||
}{
|
||||
{http.MethodPatch, "/v1/article/1?format=pdf", `{"body":"<p>B</p>"}`},
|
||||
{http.MethodPost, "/v1/article/?format=pdf", `{"title":"Deck","body":"<p>A</p>"}`},
|
||||
{http.MethodPatch, "/v1/article/1/publish?format=pdf", ""},
|
||||
{http.MethodPatch, "/v1/article/1/unpublish?format=pdf", ""},
|
||||
}
|
||||
for _, request := range requests {
|
||||
repo := seedBodyFormatRepo()
|
||||
renderer := &fakeBodyRenderer{}
|
||||
router := newRouter(repo, newPublicArticleReadUserRepo(true), renderer)
|
||||
rec := serveBodyFormat(router, request.method, request.path, request.body, verifiedCookie)
|
||||
if rec.Code != http.StatusBadRequest {
|
||||
t.Fatalf("%s %s: expected 400, got %d", request.method, request.path, rec.Code)
|
||||
}
|
||||
if rec.Body.String() != apperr.ArticleBodyFormatErrorMessage+"\n" {
|
||||
t.Errorf("%s %s: unexpected body %q", request.method, request.path, rec.Body.String())
|
||||
}
|
||||
if renderer.calls != 0 {
|
||||
t.Errorf("%s %s: renderer must not be called", request.method, request.path)
|
||||
}
|
||||
if repo.articles[1].Body != bodyFormatArticleOneHTML {
|
||||
t.Errorf("%s %s: stored body changed to %q", request.method, request.path, repo.articles[1].Body)
|
||||
}
|
||||
if !repo.articles[1].IsOnline {
|
||||
t.Errorf("%s %s: is_online changed", request.method, request.path)
|
||||
}
|
||||
if repo.createCalled {
|
||||
t.Errorf("%s %s: no article may be created", request.method, request.path)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("The authenticated list includes offline articles in the negotiated format", func(t *testing.T) {
|
||||
router, _ := realRouter()
|
||||
rec := serveBodyFormat(router, http.MethodGet, "/v1/article/?format=json", "", verifiedCookie)
|
||||
var items []bodyFormatResponse
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &items); err != nil {
|
||||
t.Fatalf("failed to decode list: %v", err)
|
||||
}
|
||||
seen := map[uint32]bool{}
|
||||
for _, item := range items {
|
||||
seen[item.ID] = true
|
||||
if item.BodyFormat != "json" {
|
||||
t.Errorf("expected json, got %q", item.BodyFormat)
|
||||
}
|
||||
}
|
||||
if !seen[1] || !seen[2] {
|
||||
t.Errorf("expected articles 1 and 2, got %v", seen)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("A converter failure answers 500", func(t *testing.T) {
|
||||
router := newRouter(seedBodyFormatRepo(), newMockUserRepo(), &fakeBodyRenderer{fail: true})
|
||||
rec := serveBodyFormat(router, http.MethodGet, "/v1/article/1", "", nil)
|
||||
if rec.Code != http.StatusInternalServerError || rec.Body.String() != "Internal server error\n" {
|
||||
t.Errorf("unexpected response %d %q", rec.Code, rec.Body.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestParseBodyFormat(t *testing.T) {
|
||||
valid := map[string]domainArticle.BodyFormat{
|
||||
"": domainArticle.BodyFormatMarkdown, "markdown": domainArticle.BodyFormatMarkdown, "MARKDOWN": domainArticle.BodyFormatMarkdown,
|
||||
"html": domainArticle.BodyFormatHTML, "Html": domainArticle.BodyFormatHTML, "json": domainArticle.BodyFormatJSON,
|
||||
}
|
||||
for input, expected := range valid {
|
||||
got, err := domainArticle.ParseBodyFormat(input)
|
||||
if err != nil || got != expected {
|
||||
t.Errorf("ParseBodyFormat(%q) = %q, %v", input, got, err)
|
||||
}
|
||||
}
|
||||
for _, input := range []string{"pdf", "md", " html"} {
|
||||
if _, err := domainArticle.ParseBodyFormat(input); !errors.Is(err, domainArticle.ErrUnsupportedBodyFormat) {
|
||||
t.Errorf("ParseBodyFormat(%q) should be unsupported, got %v", input, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,11 +23,11 @@ func newArticlePublishingRouter(articleRepo *mockArticleRepo, token *jwtauth.JWT
|
||||
getUserUseCase := useCases.NewGetUserUseCase(userRepo)
|
||||
|
||||
articleHandler := article.NewHandler(
|
||||
useCases.NewCreateArticleUseCase(articleRepo),
|
||||
useCases.NewGetArticleUseCase(articleRepo),
|
||||
useCases.NewListArticlesUseCase(articleRepo),
|
||||
useCases.NewCreateArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewGetArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewListArticlesUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewDeleteArticleUseCase(articleRepo),
|
||||
useCases.NewUpdateArticleUseCase(articleRepo),
|
||||
useCases.NewUpdateArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
getUserUseCase,
|
||||
)
|
||||
|
||||
|
||||
@@ -148,7 +148,7 @@ var _ article.Repository = &mockArticleRepo{}
|
||||
|
||||
func TestCreateArticle(t *testing.T) {
|
||||
repo := newMockArticleRepo()
|
||||
uc := useCases.NewCreateArticleUseCase(repo)
|
||||
uc := useCases.NewCreateArticleUseCase(repo, &fakeBodyRenderer{})
|
||||
|
||||
t.Run("Valid Title", func(t *testing.T) {
|
||||
cmd := useCases.CreateArticleCommand{Title: "Valid Title", Body: "Content"}
|
||||
@@ -174,17 +174,17 @@ func TestGetOnlineArticle(t *testing.T) {
|
||||
repo := newMockArticleRepo()
|
||||
repo.articles[1] = article.Article{ID: 1, IsOnline: true}
|
||||
repo.articles[2] = article.Article{ID: 2, IsOnline: false}
|
||||
uc := useCases.NewGetArticleUseCase(repo)
|
||||
uc := useCases.NewGetArticleUseCase(repo, &fakeBodyRenderer{})
|
||||
|
||||
t.Run("Article Online", func(t *testing.T) {
|
||||
_, err := uc.GetOnlineArticle(1)
|
||||
_, err := uc.GetOnlineArticle(1, article.BodyFormatHTML)
|
||||
if err != nil {
|
||||
t.Errorf("Expected article to be found")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Article Offline", func(t *testing.T) {
|
||||
_, err := uc.GetOnlineArticle(2)
|
||||
_, err := uc.GetOnlineArticle(2, article.BodyFormatHTML)
|
||||
if !errors.Is(err, article.ErrArticleNotFound) {
|
||||
t.Errorf("Offline article should return not found")
|
||||
}
|
||||
|
||||
@@ -26,11 +26,11 @@ func newArticleTitleRulesRouter(repo *mockArticleRepo, token *jwtauth.JWTAuth) *
|
||||
getUserUseCase := useCases.NewGetUserUseCase(userRepo)
|
||||
|
||||
articleHandler := article.NewHandler(
|
||||
useCases.NewCreateArticleUseCase(repo),
|
||||
useCases.NewGetArticleUseCase(repo),
|
||||
useCases.NewListArticlesUseCase(repo),
|
||||
useCases.NewCreateArticleUseCase(repo, &fakeBodyRenderer{}),
|
||||
useCases.NewGetArticleUseCase(repo, &fakeBodyRenderer{}),
|
||||
useCases.NewListArticlesUseCase(repo, &fakeBodyRenderer{}),
|
||||
useCases.NewDeleteArticleUseCase(repo),
|
||||
useCases.NewUpdateArticleUseCase(repo),
|
||||
useCases.NewUpdateArticleUseCase(repo, &fakeBodyRenderer{}),
|
||||
getUserUseCase,
|
||||
)
|
||||
|
||||
@@ -87,7 +87,7 @@ func TestArticleTitleRunesInUseCase(t *testing.T) {
|
||||
for _, tc := range cases {
|
||||
t.Run(fmt.Sprintf("rune_count_%d", tc.runeCount), func(t *testing.T) {
|
||||
repo := newMockArticleRepo()
|
||||
uc := useCases.NewCreateArticleUseCase(repo)
|
||||
uc := useCases.NewCreateArticleUseCase(repo, &fakeBodyRenderer{})
|
||||
|
||||
title := strings.Repeat("\U0001F600", tc.runeCount)
|
||||
result, err := uc.CreateArticle(useCases.CreateArticleCommand{Title: title, Body: "Some body text"})
|
||||
|
||||
@@ -20,11 +20,11 @@ func newArticlesListRouter(articleRepo *mockArticleRepo, token *jwtauth.JWTAuth)
|
||||
getUserUseCase := useCases.NewGetUserUseCase(userRepo)
|
||||
|
||||
articleHandler := article.NewHandler(
|
||||
useCases.NewCreateArticleUseCase(articleRepo),
|
||||
useCases.NewGetArticleUseCase(articleRepo),
|
||||
useCases.NewListArticlesUseCase(articleRepo),
|
||||
useCases.NewCreateArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewGetArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewListArticlesUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewDeleteArticleUseCase(articleRepo),
|
||||
useCases.NewUpdateArticleUseCase(articleRepo),
|
||||
useCases.NewUpdateArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
getUserUseCase,
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
domainArticle "RenewCMS/internal/domain/article"
|
||||
"RenewCMS/internal/infrastructure/markdown"
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const hostileBody = `<h1>Title</h1><p>Intro <strong>bold</strong><script>alert(1)</script></p><style>p{}</style><iframe src="x"></iframe><ul><li><p>one</p><ul><li><p>deep</p></li></ul></li></ul><div><span style="color:red">kept</span></div><img src="/i.png" alt="I"><pre><code>a<script>z</script></code></pre>`
|
||||
|
||||
func TestConverterNeverLeaksRawHtml(t *testing.T) {
|
||||
converter := markdown.NewConverter()
|
||||
for _, format := range []domainArticle.BodyFormat{domainArticle.BodyFormatMarkdown, domainArticle.BodyFormatJSON} {
|
||||
got, err := converter.Render(hostileBody, format)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: unexpected error: %v", format, err)
|
||||
}
|
||||
for _, forbidden := range []string{"<script", "<style", "<iframe", "<span", "alert(1)", "p{}"} {
|
||||
if strings.Contains(got, forbidden) {
|
||||
t.Errorf("%s output leaks %q: %s", format, forbidden, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestJsonConverterCombinedDocument(t *testing.T) {
|
||||
converter := markdown.NewConverter()
|
||||
got, err := converter.Render(hostileBody, domainArticle.BodyFormatJSON)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
var blocks []map[string]any
|
||||
if err := json.Unmarshal([]byte(got), &blocks); err != nil {
|
||||
t.Fatalf("body is not a JSON array: %v", err)
|
||||
}
|
||||
var types []string
|
||||
for _, b := range blocks {
|
||||
types = append(types, b["type"].(string))
|
||||
}
|
||||
expected := []string{"heading", "paragraph", "list", "paragraph", "image", "code"}
|
||||
if !reflect.DeepEqual(types, expected) {
|
||||
t.Errorf("expected block types %v, got %v", expected, types)
|
||||
}
|
||||
}
|
||||
|
||||
func TestJsonConverterEmptyBodyIsArray(t *testing.T) {
|
||||
converter := markdown.NewConverter()
|
||||
for _, source := range []string{"", " ", "<script>x</script>"} {
|
||||
got, err := converter.Render(source, domainArticle.BodyFormatJSON)
|
||||
if err != nil || got != "[]" {
|
||||
t.Errorf("%q: expected [] got %q, %v", source, got, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarkdownConverterCodeBlockText(t *testing.T) {
|
||||
converter := markdown.NewConverter()
|
||||
examples := []struct{ name, html, markdown string }{
|
||||
{"text node kept", "<pre><code>x</code></pre>", "```\nx\n```"},
|
||||
{"skipped element dropped", "<pre><code>a<script>z</script>b</code></pre>", "```\nab\n```"},
|
||||
{"style dropped", "<pre><code>a<style>z</style></code></pre>", "```\na\n```"},
|
||||
{"break becomes newline", "<pre><code>a<br>b</code></pre>", "```\na\nb\n```"},
|
||||
{"nested element text kept", "<pre><code>a<span>b</span>c</code></pre>", "```\nabc\n```"},
|
||||
{"trailing newline trimmed", "<pre><code>a\n</code></pre>", "```\na\n```"},
|
||||
{"fence grows around backticks", "<pre><code>```\nx</code></pre>", "````\n```\nx\n````"},
|
||||
}
|
||||
for _, example := range examples {
|
||||
t.Run(example.name, func(t *testing.T) {
|
||||
got, err := converter.Render(example.html, domainArticle.BodyFormatMarkdown)
|
||||
if err != nil || got != example.markdown {
|
||||
t.Errorf("expected %q, got %q, %v", example.markdown, got, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -24,11 +24,11 @@ func newEditArticleBodyRouter(articleRepo *mockArticleRepo, token *jwtauth.JWTAu
|
||||
getUserUseCase := useCases.NewGetUserUseCase(userRepo)
|
||||
|
||||
articleHandler := article.NewHandler(
|
||||
useCases.NewCreateArticleUseCase(articleRepo),
|
||||
useCases.NewGetArticleUseCase(articleRepo),
|
||||
useCases.NewListArticlesUseCase(articleRepo),
|
||||
useCases.NewCreateArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewGetArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewListArticlesUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewDeleteArticleUseCase(articleRepo),
|
||||
useCases.NewUpdateArticleUseCase(articleRepo),
|
||||
useCases.NewUpdateArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
getUserUseCase,
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
domainArticle "RenewCMS/internal/domain/article"
|
||||
"RenewCMS/internal/infrastructure/markdown"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestJsonBlocksConverter(t *testing.T) {
|
||||
converter := markdown.NewConverter()
|
||||
paragraphA := `{"type":"paragraph","children":[{"type":"text","text":"a","marks":[]}]}`
|
||||
listOfA := `[{"type":"list","ordered":false,"items":[[` + paragraphA + `]]}]`
|
||||
examples := []struct {
|
||||
name, html, json string
|
||||
}{
|
||||
{"h1", "<h1>T</h1>", `[{"type":"heading","level":1,"children":[{"type":"text","text":"T","marks":[]}]}]`},
|
||||
{"h6", "<h6>S</h6>", `[{"type":"heading","level":6,"children":[{"type":"text","text":"S","marks":[]}]}]`},
|
||||
{"paragraph", "<p>plain</p>", `[{"type":"paragraph","children":[{"type":"text","text":"plain","marks":[]}]}]`},
|
||||
{"empty paragraph", "<p></p>", `[{"type":"paragraph","children":[]}]`},
|
||||
{"bold", "<p><strong>b</strong></p>", `[{"type":"paragraph","children":[{"type":"text","text":"b","marks":[{"type":"bold"}]}]}]`},
|
||||
{"italic", "<p><em>i</em></p>", `[{"type":"paragraph","children":[{"type":"text","text":"i","marks":[{"type":"italic"}]}]}]`},
|
||||
{"link", `<p><a href="https://example.com/x">l</a></p>`, `[{"type":"paragraph","children":[{"type":"text","text":"l","marks":[{"type":"link","href":"https://example.com/x"}]}]}]`},
|
||||
{"nested marks outermost first", `<p><a href="/a"><strong><em>x</em></strong></a></p>`, `[{"type":"paragraph","children":[{"type":"text","text":"x","marks":[{"type":"link","href":"/a"},{"type":"bold"},{"type":"italic"}]}]}]`},
|
||||
{"line break", "<p>a<br>b</p>", `[{"type":"paragraph","children":[{"type":"text","text":"a\nb","marks":[]}]}]`},
|
||||
{"bullet list", "<ul><li><p>a</p></li><li><p>b</p></li></ul>", `[{"type":"list","ordered":false,"items":[[` + paragraphA + `],[{"type":"paragraph","children":[{"type":"text","text":"b","marks":[]}]}]]}]`},
|
||||
{"ordered list", "<ol><li><p>a</p></li></ol>", `[{"type":"list","ordered":true,"items":[[` + paragraphA + `]]}]`},
|
||||
{"bare list item", "<ul><li>a</li></ul>", listOfA},
|
||||
{"nested list", "<ul><li><p>a</p><ul><li><p>n</p></li></ul></li></ul>", `[{"type":"list","ordered":false,"items":[[` + paragraphA + `,{"type":"list","ordered":false,"items":[[{"type":"paragraph","children":[{"type":"text","text":"n","marks":[]}]}]]}]]}]`},
|
||||
{"data-indent ignored", `<ul><li data-indent="2"><p>a</p></li></ul>`, listOfA},
|
||||
{"blockquote", "<blockquote><p>q</p></blockquote>", `[{"type":"blockquote","children":[{"type":"paragraph","children":[{"type":"text","text":"q","marks":[]}]}]}]`},
|
||||
{"code with language", `<pre><code class="language-go">x := 1</code></pre>`, `[{"type":"code","language":"go","text":"x := 1"}]`},
|
||||
{"code without language", "<pre><code>raw</code></pre>", `[{"type":"code","language":"","text":"raw"}]`},
|
||||
{"rule", "<hr>", `[{"type":"hr"}]`},
|
||||
{"image with alt", `<img src="/static/uploadedImages/a.png" alt="A">`, `[{"type":"image","src":"/static/uploadedImages/a.png","alt":"A"}]`},
|
||||
{"image without alt", `<img src="/static/uploadedImages/a.png">`, `[{"type":"image","src":"/static/uploadedImages/a.png","alt":""}]`},
|
||||
{"image splits paragraph", `<p>x<img src="/i.png" alt="I">y</p>`, `[{"type":"paragraph","children":[{"type":"text","text":"x","marks":[]}]},{"type":"image","src":"/i.png","alt":"I"},{"type":"paragraph","children":[{"type":"text","text":"y","marks":[]}]}]`},
|
||||
{"span stripped", `<p>x<span style="color:red">y</span></p>`, `[{"type":"paragraph","children":[{"type":"text","text":"xy","marks":[]}]}]`},
|
||||
{"inline code degrades", "<p>use <code>go test</code></p>", `[{"type":"paragraph","children":[{"type":"text","text":"use go test","marks":[]}]}]`},
|
||||
{"script and style removed", "<p>ok</p><script>alert(1)</script><style>p{}</style>", `[{"type":"paragraph","children":[{"type":"text","text":"ok","marks":[]}]}]`},
|
||||
{"empty html", "", `[]`},
|
||||
{"centered paragraph", `<p style="text-align: center">a</p>`, `[{"type":"paragraph","align":"center","children":[{"type":"text","text":"a","marks":[]}]}]`},
|
||||
{"right aligned heading", `<h2 style="text-align:right">T</h2>`, `[{"type":"heading","level":2,"align":"right","children":[{"type":"text","text":"T","marks":[]}]}]`},
|
||||
{"justified paragraph", `<p style="color:red; text-align: JUSTIFY">a</p>`, `[{"type":"paragraph","align":"justify","children":[{"type":"text","text":"a","marks":[]}]}]`},
|
||||
{"left alignment omitted", `<p style="text-align: left">a</p>`, `[` + paragraphA + `]`},
|
||||
{"alignment skips the image inside a paragraph", `<p style="text-align: center">x<img src="/i.png" alt="I">y</p>`, `[{"type":"paragraph","align":"center","children":[{"type":"text","text":"x","marks":[]}]},{"type":"image","src":"/i.png","alt":"I"},{"type":"paragraph","align":"center","children":[{"type":"text","text":"y","marks":[]}]}]`},
|
||||
}
|
||||
for _, example := range examples {
|
||||
t.Run(example.name, func(t *testing.T) {
|
||||
got, err := converter.Render(example.html, domainArticle.BodyFormatJSON)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if got != example.json {
|
||||
t.Errorf("expected %s, got %s", example.json, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
domainArticle "RenewCMS/internal/domain/article"
|
||||
"RenewCMS/internal/infrastructure/markdown"
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMarkdownConverterFidelity(t *testing.T) {
|
||||
converter := markdown.NewConverter()
|
||||
examples := []struct {
|
||||
name, html, markdown string
|
||||
}{
|
||||
{"headings", "<h1>T</h1><h3>S</h3>", "# T\n\n### S"},
|
||||
{"bold and italic", "<p><strong>b</strong> <em>i</em></p>", "**b** *i*"},
|
||||
{"link", `<p><a href="https://example.com/x">link</a></p>`, "[link](https://example.com/x)"},
|
||||
{"bullet list", "<ul><li><p>a</p></li><li><p>b</p></li></ul>", "- a\n- b"},
|
||||
{"ordered list", "<ol><li><p>a</p></li><li><p>b</p></li></ol>", "1. a\n2. b"},
|
||||
{"data-indent dropped", `<ul><li data-indent="2"><p>a</p></li></ul>`, "- a"},
|
||||
{"nested list", "<ul><li><p>a</p><ul><li><p>n</p></li></ul></li></ul>", "- a\n - n"},
|
||||
{"blockquote", "<blockquote><p>q</p></blockquote>", "> q"},
|
||||
{"code block", `<pre><code class="language-go">x := 1</code></pre>`, "```go\nx := 1\n```"},
|
||||
{"rule", "<hr>", "---"},
|
||||
{"image", `<img src="/static/uploadedImages/a.png" alt="A">`, ""},
|
||||
{"script and style removed", "<p>ok</p><script>alert(1)</script><style>p{}</style>", "ok"},
|
||||
{"unsupported span stripped", `<p>x<span style="color:red">y</span></p>`, "xy"},
|
||||
{"inline code", "<p>use <code>go test</code></p>", "use `go test`"},
|
||||
{"empty body", "", ""},
|
||||
{"line break", "<p>a<br>b</p>", "a\\\nb"},
|
||||
{"literal angle bracket is escaped", "<p><script></p>", "\\<script>"},
|
||||
{"javascript link dropped", `<p><a href="javascript:alert(1)">x</a></p>`, "x"},
|
||||
{"bold keeps outer spaces", "<p>a<strong> b </strong>c</p>", "a **b** c"},
|
||||
{"loose list item", "<ul><li><p>a</p><p>b</p></li></ul>", "- a\n\n b"},
|
||||
{"inline code with edge backtick", "<p><code>`x</code></p>", "`` `x ``"},
|
||||
{"code block keeps br and drops script", "<pre><code>a<br>b<script>z</script></code></pre>", "```\na\nb\n```"},
|
||||
{"adjacent same marks merge", "<p><strong>a</strong><strong>b</strong></p>", "**ab**"},
|
||||
{"adjacent different marks stay apart", "<p><strong>a</strong><em>b</em></p>", "**a***b*"},
|
||||
{"two paragraphs", "<p>a</p><p>b</p>", "a\n\nb"},
|
||||
{"centered paragraph", `<p style="text-align: center">hi <strong>b</strong></p>`, "<div style=\"text-align: center\">\n\nhi **b**\n\n</div>"},
|
||||
{"right aligned heading", `<h2 style="text-align:right">T</h2>`, "<div style=\"text-align: right\">\n\n## T\n\n</div>"},
|
||||
{"justified among other styles", `<p style="color:red; TEXT-ALIGN: Justify">a</p>`, "<div style=\"text-align: justify\">\n\na\n\n</div>"},
|
||||
{"tiptap style with trailing semicolon", `<p style="text-align: center;">a</p>`, "<div style=\"text-align: center\">\n\na\n\n</div>"},
|
||||
{"tiptap explicit left", `<p style="text-align: left;">a</p>`, "a"},
|
||||
{"left alignment is the default", `<p style="text-align: left">a</p>`, "a"},
|
||||
{"unknown alignment ignored", `<p style="text-align: start">a</p>`, "a"},
|
||||
{"style without value ignored", `<p style="text-align">a</p>`, "a"},
|
||||
{"empty aligned paragraph", `<p style="text-align: center"></p>`, ""},
|
||||
{"aligned paragraph in list item", `<ul><li><p style="text-align: center">a</p></li></ul>`, "- <div style=\"text-align: center\">\n\n a\n\n </div>"},
|
||||
{"aligned paragraph in blockquote", `<blockquote><p style="text-align: center">q</p></blockquote>`, "> <div style=\"text-align: center\">\n>\n> q\n>\n> </div>"},
|
||||
{"alignment skips the image inside a paragraph", `<p style="text-align: center">x<img src="/i.png" alt="I">y</p>`, "<div style=\"text-align: center\">\n\nx\n\n</div>\n\n\n\n<div style=\"text-align: center\">\n\ny\n\n</div>"},
|
||||
{"text characters are escaped", "<p>a * b _c_ `d` [e]</p>", "a \\* b \\_c\\_ \\`d\\` \\[e\\]"},
|
||||
{"leading hash is escaped", "<p># not a heading</p>", "\\# not a heading"},
|
||||
{"hash without space is kept", "<p>#tag</p>", "#tag"},
|
||||
{"leading dash is escaped", "<p>- not a list</p>", "\\- not a list"},
|
||||
{"leading plus is escaped", "<p>+ not a list</p>", "\\+ not a list"},
|
||||
{"leading quote marker is escaped", "<p>> not a quote</p>", "\\> not a quote"},
|
||||
{"leading number is escaped", "<p>1. not a list</p>", "1\\. not a list"},
|
||||
{"leading number with parenthesis is escaped", "<p>2) not a list</p>", "2\\) not a list"},
|
||||
{"leading rule is escaped", "<p>---</p>", "\\---"},
|
||||
{"leading equals is escaped", "<p>==</p>", "\\=="},
|
||||
{"inner dash is kept", "<p>a - b</p>", "a - b"},
|
||||
{"marker after a line break is escaped", "<p>a<br>- b</p>", "a\\\n\\- b"},
|
||||
{"combined document", `<h1>Title</h1><p style="text-align: center">Intro <strong>bold</strong> and <em>it</em></p><ul><li><p>one</p></li><li><p>two</p></li></ul><img src="/static/uploadedImages/a.png" alt="A"><pre><code class="language-go">x := 1</code></pre><blockquote><p>quote</p></blockquote><hr>`, "# Title\n\n<div style=\"text-align: center\">\n\nIntro **bold** and *it*\n\n</div>\n\n- one\n- two\n\n\n\n```go\nx := 1\n```\n\n> quote\n\n---"},
|
||||
}
|
||||
for _, example := range examples {
|
||||
t.Run(example.name, func(t *testing.T) {
|
||||
got, err := converter.Render(example.html, domainArticle.BodyFormatMarkdown)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if got != example.markdown {
|
||||
t.Errorf("expected %q, got %q", example.markdown, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarkdownConverterHtmlAndUnknownFormat(t *testing.T) {
|
||||
converter := markdown.NewConverter()
|
||||
html := "<p>a</p><script>x</script>"
|
||||
got, err := converter.Render(html, domainArticle.BodyFormatHTML)
|
||||
if err != nil || got != html {
|
||||
t.Errorf("html must pass through unchanged, got %q, %v", got, err)
|
||||
}
|
||||
if _, err := converter.Render(html, domainArticle.BodyFormat("pdf")); !errors.Is(err, domainArticle.ErrUnsupportedBodyFormat) {
|
||||
t.Errorf("expected ErrUnsupportedBodyFormat, got %v", err)
|
||||
}
|
||||
}
|
||||
@@ -52,10 +52,10 @@ func newMediaEnv(t *testing.T) *mediaEnv {
|
||||
|
||||
container := &api.UseCases{
|
||||
Token: env.token,
|
||||
CreateArticleUseCase: useCases.NewCreateArticleUseCase(env.articles),
|
||||
GetArticleUseCase: useCases.NewGetArticleUseCase(env.articles),
|
||||
ListArticlesUseCase: useCases.NewListArticlesUseCase(env.articles),
|
||||
UpdateArticleUseCase: useCases.NewUpdateArticleUseCase(env.articles),
|
||||
CreateArticleUseCase: useCases.NewCreateArticleUseCase(env.articles, &fakeBodyRenderer{}),
|
||||
GetArticleUseCase: useCases.NewGetArticleUseCase(env.articles, &fakeBodyRenderer{}),
|
||||
ListArticlesUseCase: useCases.NewListArticlesUseCase(env.articles, &fakeBodyRenderer{}),
|
||||
UpdateArticleUseCase: useCases.NewUpdateArticleUseCase(env.articles, &fakeBodyRenderer{}),
|
||||
DeleteArticleUseCase: useCases.NewDeleteArticleUseCase(env.articles),
|
||||
GetUserUseCase: useCases.NewGetUserUseCase(env.users),
|
||||
CreateImageUseCase: useCases.NewCreateImageUseCase(env.images),
|
||||
|
||||
@@ -126,11 +126,11 @@ func TestUserRepositoryReportsMissingUserAsDomainError(t *testing.T) {
|
||||
func newNotFoundErrorsRouter(articleRepo *mockArticleRepo, userRepo *mockUserRepo, token *jwtauth.JWTAuth) *chi.Mux {
|
||||
getUserUseCase := useCases.NewGetUserUseCase(userRepo)
|
||||
articleHandler := article.NewHandler(
|
||||
useCases.NewCreateArticleUseCase(articleRepo),
|
||||
useCases.NewGetArticleUseCase(articleRepo),
|
||||
useCases.NewListArticlesUseCase(articleRepo),
|
||||
useCases.NewCreateArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewGetArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewListArticlesUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewDeleteArticleUseCase(articleRepo),
|
||||
useCases.NewUpdateArticleUseCase(articleRepo),
|
||||
useCases.NewUpdateArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
getUserUseCase,
|
||||
)
|
||||
hasher := hashing.NewBcryptHasher()
|
||||
|
||||
@@ -42,11 +42,11 @@ func newPublicArticleListingRouter(articleRepo *mockArticleRepo, token *jwtauth.
|
||||
getUserUseCase := useCases.NewGetUserUseCase(userRepo)
|
||||
|
||||
articleHandler := article.NewHandler(
|
||||
useCases.NewCreateArticleUseCase(articleRepo),
|
||||
useCases.NewGetArticleUseCase(articleRepo),
|
||||
useCases.NewListArticlesUseCase(articleRepo),
|
||||
useCases.NewCreateArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewGetArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewListArticlesUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewDeleteArticleUseCase(articleRepo),
|
||||
useCases.NewUpdateArticleUseCase(articleRepo),
|
||||
useCases.NewUpdateArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
getUserUseCase,
|
||||
)
|
||||
|
||||
|
||||
@@ -24,11 +24,11 @@ import (
|
||||
func newPublicArticleReadRouter(articleRepo *mockArticleRepo, userRepo *mockUserRepo, token *jwtauth.JWTAuth) *chi.Mux {
|
||||
getUserUseCase := useCases.NewGetUserUseCase(userRepo)
|
||||
articleHandler := article.NewHandler(
|
||||
useCases.NewCreateArticleUseCase(articleRepo),
|
||||
useCases.NewGetArticleUseCase(articleRepo),
|
||||
useCases.NewListArticlesUseCase(articleRepo),
|
||||
useCases.NewCreateArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewGetArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewListArticlesUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewDeleteArticleUseCase(articleRepo),
|
||||
useCases.NewUpdateArticleUseCase(articleRepo),
|
||||
useCases.NewUpdateArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
getUserUseCase,
|
||||
)
|
||||
|
||||
|
||||
@@ -132,10 +132,10 @@ func newRuntimeSettingsContainer(t *testing.T) *api.UseCases {
|
||||
|
||||
return &api.UseCases{
|
||||
Token: token,
|
||||
CreateArticleUseCase: useCases.NewCreateArticleUseCase(articleRepo),
|
||||
GetArticleUseCase: useCases.NewGetArticleUseCase(articleRepo),
|
||||
ListArticlesUseCase: useCases.NewListArticlesUseCase(articleRepo),
|
||||
UpdateArticleUseCase: useCases.NewUpdateArticleUseCase(articleRepo),
|
||||
CreateArticleUseCase: useCases.NewCreateArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
GetArticleUseCase: useCases.NewGetArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
ListArticlesUseCase: useCases.NewListArticlesUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
UpdateArticleUseCase: useCases.NewUpdateArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
DeleteArticleUseCase: useCases.NewDeleteArticleUseCase(articleRepo),
|
||||
GetUserUseCase: useCases.NewGetUserUseCase(userRepo),
|
||||
CreateUserUseCase: useCases.NewCreateUserUseCase(userRepo, hasher, mailRepo, "https://cms.example.com"),
|
||||
|
||||
@@ -23,11 +23,11 @@ import (
|
||||
func newStaleTokenInvalidationRouter(userRepo *mockUserRepo, articleRepo *mockArticleRepo, imageRepo *mockImageRepo, mailRepo *mockMailRepo, hasher *stubHasher, token *jwtauth.JWTAuth) *chi.Mux {
|
||||
getUserUseCase := useCases.NewGetUserUseCase(userRepo)
|
||||
articleHandler := article.NewHandler(
|
||||
useCases.NewCreateArticleUseCase(articleRepo),
|
||||
useCases.NewGetArticleUseCase(articleRepo),
|
||||
useCases.NewListArticlesUseCase(articleRepo),
|
||||
useCases.NewCreateArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewGetArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewListArticlesUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewDeleteArticleUseCase(articleRepo),
|
||||
useCases.NewUpdateArticleUseCase(articleRepo),
|
||||
useCases.NewUpdateArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
getUserUseCase,
|
||||
)
|
||||
loginHandler := auth.NewLoginHandler(
|
||||
|
||||
@@ -29,7 +29,7 @@ func TestCreateArticleTitleLengthLimit(t *testing.T) {
|
||||
for _, tc := range cases {
|
||||
t.Run(fmt.Sprintf("length_%d", tc.length), func(t *testing.T) {
|
||||
repo := newMockArticleRepo()
|
||||
uc := useCases.NewCreateArticleUseCase(repo)
|
||||
uc := useCases.NewCreateArticleUseCase(repo, &fakeBodyRenderer{})
|
||||
|
||||
title := strings.Repeat("a", tc.length)
|
||||
result, err := uc.CreateArticle(useCases.CreateArticleCommand{Title: title, Body: "Some body text"})
|
||||
@@ -62,7 +62,7 @@ func TestCreateArticleTitleLengthLimit(t *testing.T) {
|
||||
func TestCreateArticlePropagatesRepositoryFailure(t *testing.T) {
|
||||
repo := newMockArticleRepo()
|
||||
repo.fail.failNext("Create")
|
||||
uc := useCases.NewCreateArticleUseCase(repo)
|
||||
uc := useCases.NewCreateArticleUseCase(repo, &fakeBodyRenderer{})
|
||||
|
||||
_, err := uc.CreateArticle(useCases.CreateArticleCommand{Title: "Backyard Renovation Update", Body: "Some body text"})
|
||||
|
||||
@@ -84,8 +84,8 @@ func TestGetArticleAndGetOnlineArticle(t *testing.T) {
|
||||
|
||||
t.Run("GetArticle_1", func(t *testing.T) {
|
||||
repo := newRepo()
|
||||
uc := useCases.NewGetArticleUseCase(repo)
|
||||
result, err := uc.GetArticle(1)
|
||||
uc := useCases.NewGetArticleUseCase(repo, &fakeBodyRenderer{})
|
||||
result, err := uc.GetArticle(1, article.BodyFormatHTML)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
@@ -96,8 +96,8 @@ func TestGetArticleAndGetOnlineArticle(t *testing.T) {
|
||||
|
||||
t.Run("GetArticle_2", func(t *testing.T) {
|
||||
repo := newRepo()
|
||||
uc := useCases.NewGetArticleUseCase(repo)
|
||||
result, err := uc.GetArticle(2)
|
||||
uc := useCases.NewGetArticleUseCase(repo, &fakeBodyRenderer{})
|
||||
result, err := uc.GetArticle(2, article.BodyFormatHTML)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
@@ -108,8 +108,8 @@ func TestGetArticleAndGetOnlineArticle(t *testing.T) {
|
||||
|
||||
t.Run("GetArticle_9", func(t *testing.T) {
|
||||
repo := newRepo()
|
||||
uc := useCases.NewGetArticleUseCase(repo)
|
||||
_, err := uc.GetArticle(9)
|
||||
uc := useCases.NewGetArticleUseCase(repo, &fakeBodyRenderer{})
|
||||
_, err := uc.GetArticle(9, article.BodyFormatHTML)
|
||||
if !errors.Is(err, article.ErrArticleNotFound) {
|
||||
t.Errorf("expected article.ErrArticleNotFound, got %v", err)
|
||||
}
|
||||
@@ -117,8 +117,8 @@ func TestGetArticleAndGetOnlineArticle(t *testing.T) {
|
||||
|
||||
t.Run("GetOnlineArticle_1", func(t *testing.T) {
|
||||
repo := newRepo()
|
||||
uc := useCases.NewGetArticleUseCase(repo)
|
||||
result, err := uc.GetOnlineArticle(1)
|
||||
uc := useCases.NewGetArticleUseCase(repo, &fakeBodyRenderer{})
|
||||
result, err := uc.GetOnlineArticle(1, article.BodyFormatHTML)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
@@ -129,8 +129,8 @@ func TestGetArticleAndGetOnlineArticle(t *testing.T) {
|
||||
|
||||
t.Run("GetOnlineArticle_2", func(t *testing.T) {
|
||||
repo := newRepo()
|
||||
uc := useCases.NewGetArticleUseCase(repo)
|
||||
_, err := uc.GetOnlineArticle(2)
|
||||
uc := useCases.NewGetArticleUseCase(repo, &fakeBodyRenderer{})
|
||||
_, err := uc.GetOnlineArticle(2, article.BodyFormatHTML)
|
||||
if !errors.Is(err, article.ErrArticleNotFound) {
|
||||
t.Errorf("expected article.ErrArticleNotFound, got %v", err)
|
||||
}
|
||||
@@ -138,8 +138,8 @@ func TestGetArticleAndGetOnlineArticle(t *testing.T) {
|
||||
|
||||
t.Run("GetOnlineArticle_9", func(t *testing.T) {
|
||||
repo := newRepo()
|
||||
uc := useCases.NewGetArticleUseCase(repo)
|
||||
_, err := uc.GetOnlineArticle(9)
|
||||
uc := useCases.NewGetArticleUseCase(repo, &fakeBodyRenderer{})
|
||||
_, err := uc.GetOnlineArticle(9, article.BodyFormatHTML)
|
||||
if !errors.Is(err, article.ErrArticleNotFound) {
|
||||
t.Errorf("expected article.ErrArticleNotFound, got %v", err)
|
||||
}
|
||||
@@ -155,7 +155,7 @@ func TestGetArticleByNameDelegatesVerbatim(t *testing.T) {
|
||||
|
||||
t.Run("Backyard_Renovation_Update", func(t *testing.T) {
|
||||
repo := newRepo()
|
||||
uc := useCases.NewGetArticleUseCase(repo)
|
||||
uc := useCases.NewGetArticleUseCase(repo, &fakeBodyRenderer{})
|
||||
result, err := uc.GetArticleByName("Backyard Renovation Update")
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
@@ -167,7 +167,7 @@ func TestGetArticleByNameDelegatesVerbatim(t *testing.T) {
|
||||
|
||||
t.Run("Unknown_Title", func(t *testing.T) {
|
||||
repo := newRepo()
|
||||
uc := useCases.NewGetArticleUseCase(repo)
|
||||
uc := useCases.NewGetArticleUseCase(repo, &fakeBodyRenderer{})
|
||||
_, err := uc.GetArticleByName("Unknown Title")
|
||||
if !errors.Is(err, article.ErrArticleNotFound) {
|
||||
t.Errorf("expected article.ErrArticleNotFound, got %v", err)
|
||||
@@ -198,8 +198,8 @@ func TestListArticlesFiltersByIsOnline(t *testing.T) {
|
||||
|
||||
t.Run("ListOnlineArticles", func(t *testing.T) {
|
||||
repo := newRepo()
|
||||
uc := useCases.NewListArticlesUseCase(repo)
|
||||
result, err := uc.ListOnlineArticles()
|
||||
uc := useCases.NewListArticlesUseCase(repo, &fakeBodyRenderer{})
|
||||
result, err := uc.ListOnlineArticles(article.BodyFormatHTML)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
@@ -208,8 +208,8 @@ func TestListArticlesFiltersByIsOnline(t *testing.T) {
|
||||
|
||||
t.Run("ListAllArticles", func(t *testing.T) {
|
||||
repo := newRepo()
|
||||
uc := useCases.NewListArticlesUseCase(repo)
|
||||
result, err := uc.ListAllArticles()
|
||||
uc := useCases.NewListArticlesUseCase(repo, &fakeBodyRenderer{})
|
||||
result, err := uc.ListAllArticles(article.BodyFormatHTML)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
@@ -219,9 +219,9 @@ func TestListArticlesFiltersByIsOnline(t *testing.T) {
|
||||
|
||||
func TestListOnlineArticlesEmptyRepositoryReturnsEmptySlice(t *testing.T) {
|
||||
repo := newMockArticleRepo()
|
||||
uc := useCases.NewListArticlesUseCase(repo)
|
||||
uc := useCases.NewListArticlesUseCase(repo, &fakeBodyRenderer{})
|
||||
|
||||
result, err := uc.ListOnlineArticles()
|
||||
result, err := uc.ListOnlineArticles(article.BodyFormatHTML)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
@@ -242,8 +242,8 @@ func TestUpdateArticleDelegatesEachOperation(t *testing.T) {
|
||||
|
||||
t.Run("UpdateBody_id1", func(t *testing.T) {
|
||||
repo := newRepo()
|
||||
uc := useCases.NewUpdateArticleUseCase(repo)
|
||||
result, err := uc.UpdateBody(1, "New body")
|
||||
uc := useCases.NewUpdateArticleUseCase(repo, &fakeBodyRenderer{})
|
||||
result, err := uc.UpdateBody(1, "New body", article.BodyFormatHTML)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
@@ -254,8 +254,8 @@ func TestUpdateArticleDelegatesEachOperation(t *testing.T) {
|
||||
|
||||
t.Run("UpdateBody_id9", func(t *testing.T) {
|
||||
repo := newRepo()
|
||||
uc := useCases.NewUpdateArticleUseCase(repo)
|
||||
_, err := uc.UpdateBody(9, "New body")
|
||||
uc := useCases.NewUpdateArticleUseCase(repo, &fakeBodyRenderer{})
|
||||
_, err := uc.UpdateBody(9, "New body", article.BodyFormatHTML)
|
||||
if !errors.Is(err, article.ErrArticleNotFound) {
|
||||
t.Errorf("expected article.ErrArticleNotFound, got %v", err)
|
||||
}
|
||||
@@ -263,8 +263,8 @@ func TestUpdateArticleDelegatesEachOperation(t *testing.T) {
|
||||
|
||||
t.Run("UpdateIsOnline_id1", func(t *testing.T) {
|
||||
repo := newRepo()
|
||||
uc := useCases.NewUpdateArticleUseCase(repo)
|
||||
result, err := uc.UpdateIsOnline(1, true)
|
||||
uc := useCases.NewUpdateArticleUseCase(repo, &fakeBodyRenderer{})
|
||||
result, err := uc.UpdateIsOnline(1, true, article.BodyFormatHTML)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
@@ -275,8 +275,8 @@ func TestUpdateArticleDelegatesEachOperation(t *testing.T) {
|
||||
|
||||
t.Run("UpdateIsOnline_id9", func(t *testing.T) {
|
||||
repo := newRepo()
|
||||
uc := useCases.NewUpdateArticleUseCase(repo)
|
||||
_, err := uc.UpdateIsOnline(9, true)
|
||||
uc := useCases.NewUpdateArticleUseCase(repo, &fakeBodyRenderer{})
|
||||
_, err := uc.UpdateIsOnline(9, true, article.BodyFormatHTML)
|
||||
if !errors.Is(err, article.ErrArticleNotFound) {
|
||||
t.Errorf("expected article.ErrArticleNotFound, got %v", err)
|
||||
}
|
||||
|
||||
@@ -21,11 +21,11 @@ import (
|
||||
func newVerifiedAccountRequiredRouter(articleRepo *mockArticleRepo, userRepo *mockUserRepo, token *jwtauth.JWTAuth) *chi.Mux {
|
||||
getUserUseCase := useCases.NewGetUserUseCase(userRepo)
|
||||
articleHandler := article.NewHandler(
|
||||
useCases.NewCreateArticleUseCase(articleRepo),
|
||||
useCases.NewGetArticleUseCase(articleRepo),
|
||||
useCases.NewListArticlesUseCase(articleRepo),
|
||||
useCases.NewCreateArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewGetArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewListArticlesUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewDeleteArticleUseCase(articleRepo),
|
||||
useCases.NewUpdateArticleUseCase(articleRepo),
|
||||
useCases.NewUpdateArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
getUserUseCase,
|
||||
)
|
||||
|
||||
|
||||
@@ -213,11 +213,11 @@ func newDeleteArticleRouter(articleRepo *mockArticleRepo, token *jwtauth.JWTAuth
|
||||
getUserUseCase := useCases.NewGetUserUseCase(userRepo)
|
||||
|
||||
articleHandler := article.NewHandler(
|
||||
useCases.NewCreateArticleUseCase(articleRepo),
|
||||
useCases.NewGetArticleUseCase(articleRepo),
|
||||
useCases.NewListArticlesUseCase(articleRepo),
|
||||
useCases.NewCreateArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewGetArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewListArticlesUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewDeleteArticleUseCase(articleRepo),
|
||||
useCases.NewUpdateArticleUseCase(articleRepo),
|
||||
useCases.NewUpdateArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
getUserUseCase,
|
||||
)
|
||||
|
||||
|
||||
Reference in new issue
Block a user