refac: authenticated user can publish and unpublish an article; an unknown id returns 404

This commit is contained in:
fsylvain committed 2026-09-23 22:54:44 +02:00
1 parent 790d4e6da6
commit 8822bdddd0
9 files changed
+538 -6

No files matched your search

+48 -5
View File
@@ -21,6 +21,7 @@ type Handler struct {
GetUseCase *useCases.GetArticleUseCase
ListUseCase *useCases.ListArticlesUseCase
DeleteUseCase *useCases.DeleteArticleUseCase
UpdateUseCase *useCases.UpdateArticleUseCase
}
type PostArticle struct {
@@ -28,23 +29,29 @@ type PostArticle struct {
Body string `json:"body" validate:"required,max=10000"`
}
func NewHandler(create *useCases.CreateArticleUseCase, get *useCases.GetArticleUseCase, list *useCases.ListArticlesUseCase, delete *useCases.DeleteArticleUseCase) *Handler {
func NewHandler(create *useCases.CreateArticleUseCase, get *useCases.GetArticleUseCase, list *useCases.ListArticlesUseCase, delete *useCases.DeleteArticleUseCase, update *useCases.UpdateArticleUseCase) *Handler {
return &Handler{
CreateUseCase: create,
GetUseCase: get,
ListUseCase: list,
DeleteUseCase: delete,
UpdateUseCase: update,
}
}
func (h *Handler) GetArticle(w http.ResponseWriter, r *http.Request) {
func parseArticleId(r *http.Request) (uint32, error) {
id, err := strconv.ParseUint(chi.URLParam(r, "id"), 10, 32)
return uint32(id), err
}
func (h *Handler) GetArticle(w http.ResponseWriter, r *http.Request) {
id, err := parseArticleId(r)
if err != nil {
http.Error(w, apperr.IdUint32ErrorMessage, http.StatusBadRequest)
return
}
article, err := h.GetUseCase.GetOnlineArticle(uint32(id))
article, err := h.GetUseCase.GetOnlineArticle(id)
if err != nil {
if errors.Is(err, domain.ErrArticleNotFound) {
http.Error(w, apperr.ArticleNotFoundErrorMessage, http.StatusNotFound)
@@ -102,6 +109,42 @@ func (h *Handler) PostArticle(w http.ResponseWriter, r *http.Request) {
}
}
func (h *Handler) setIsOnline(w http.ResponseWriter, r *http.Request, isOnline bool) {
id, err := parseArticleId(r)
if err != nil {
http.Error(w, apperr.IdUint32ErrorMessage, http.StatusBadRequest)
return
}
updatedArticle, err := h.UpdateUseCase.UpdateIsOnline(id, isOnline)
if err != nil {
if errors.Is(err, domain.ErrArticleNotFound) {
http.Error(w, apperr.ArticleNotFoundErrorMessage, http.StatusNotFound)
return
}
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
articleJson, err := json.Marshal(updatedArticle)
if err != nil {
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
return
}
if _, err := w.Write(articleJson); err != nil {
log.Printf("Failed to write response: %v", err)
}
}
func (h *Handler) PublishArticle(w http.ResponseWriter, r *http.Request) {
h.setIsOnline(w, r, true)
}
func (h *Handler) UnpublishArticle(w http.ResponseWriter, r *http.Request) {
h.setIsOnline(w, r, false)
}
func requestHasValidJwt(r *http.Request) bool {
token, _, err := jwtauth.FromContext(r.Context())
return token != nil && err == nil
@@ -132,13 +175,13 @@ func (h *Handler) ListArticles(w http.ResponseWriter, r *http.Request) {
}
func (h *Handler) DeleteArticle(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseUint(chi.URLParam(r, "id"), 10, 32)
id, err := parseArticleId(r)
if err != nil {
http.Error(w, apperr.IdUint32ErrorMessage, http.StatusBadRequest)
return
}
if err := h.DeleteUseCase.DeleteArticle(uint32(id)); err != nil {
if err := h.DeleteUseCase.DeleteArticle(id); err != nil {
if errors.Is(err, domain.ErrArticleNotFound) {
http.Error(w, apperr.ArticleNotFoundErrorMessage, http.StatusNotFound)
return
+3 -1
View File
@@ -31,7 +31,7 @@ func GetHelloWorld(w http.ResponseWriter, _ *http.Request) {
func NewArticleRouter(container *UseCases) http.Handler {
r := chi.NewRouter()
articleHandler := article.NewHandler(container.CreateArticleUseCase, container.GetArticleUseCase, container.ListArticlesUseCase, container.DeleteArticleUseCase)
articleHandler := article.NewHandler(container.CreateArticleUseCase, container.GetArticleUseCase, container.ListArticlesUseCase, container.DeleteArticleUseCase, container.UpdateArticleUseCase)
r.Group(func(r chi.Router) {
r.Get("/", articleHandler.ListArticles)
@@ -41,6 +41,8 @@ func NewArticleRouter(container *UseCases) http.Handler {
r.Get("/{id}", articleHandler.GetArticle)
r.Post("/", articleHandler.PostArticle)
r.Delete("/{id}", articleHandler.DeleteArticle)
r.Patch("/{id}/publish", articleHandler.PublishArticle)
r.Patch("/{id}/unpublish", articleHandler.UnpublishArticle)
})
return r
}
+109
View File
@@ -0,0 +1,109 @@
Feature: An authenticated user can publish and unpublish an article
As an authenticated RenewCMS user managing the CMS
I want to flip an article's is_online flag from an HTTP endpoint
So that a draft can be published to the public site and a published article can be
pulled back offline, without deleting it
Background:
Given internal/domain/article.Repository already exposes
UpdateIsOnline(id uint32, isOnline bool) (Article, error), and
UpdateArticleUseCase.UpdateIsOnline delegates to it unchanged
And UpdateArticleUseCase is already built in api/dependecyInjection.go as
container.UpdateArticleUseCase, but before this slice no handler or route calls it
And this slice adds Handler.PublishArticle and Handler.UnpublishArticle, backed by
UpdateArticleUseCase.UpdateIsOnline(id, true) and UpdateIsOnline(id, false)
And this slice mounts PATCH /v1/article/{id}/publish and
PATCH /v1/article/{id}/unpublish inside the same jwtauth Verifier + Authenticator
group as GET /v1/article/{id}, POST /v1/article/ and DELETE /v1/article/{id} in
api/router.go, so both require a valid jwt cookie
And a successful publish or unpublish answers 200 with the JSON body of the updated
article.Article, the same shape GetArticle and PostArticle already return
And an id that does not parse as a uint32 answers 400 with body
apperr.IdUint32ErrorMessage, the same as GetArticle and DeleteArticle
And an id absent from the article repository answers 404 with body
apperr.ArticleNotFoundErrorMessage, backed by errors.Is(err, article.ErrArticleNotFound),
the same as DeleteArticle already does per features/write_not_found_errors.feature
And GET /v1/article/ with no jwt cookie only returns online articles, per
features/public_article_listing.feature; this slice does not change that behavior,
only feeds it
Scenario: Publishing an offline article makes it online
Given a valid jwt cookie for a user with id 1
And the article repository contains an offline article titled "Draft: Kitchen Remodel" with id 2
When the client sends PATCH /v1/article/2/publish with that cookie
Then the response status is 200
And the response body is the article with id 2 and "is_online": true
Scenario: Unpublishing an online article makes it offline
Given a valid jwt cookie for a user with id 1
And the article repository contains an online article titled "Backyard Renovation Update" with id 1
When the client sends PATCH /v1/article/1/unpublish with that cookie
Then the response status is 200
And the response body is the article with id 1 and "is_online": false
Scenario: Publishing an already-online article is idempotent
Given a valid jwt cookie for a user with id 1
And the article repository contains an online article with id 1
When the client sends PATCH /v1/article/1/publish with that cookie
Then the response status is 200
And the response body is the article with id 1 and "is_online": true
And no error is returned
Scenario: Unpublishing an already-offline article is idempotent
Given a valid jwt cookie for a user with id 1
And the article repository contains an offline article with id 2
When the client sends PATCH /v1/article/2/unpublish with that cookie
Then the response status is 200
And the response body is the article with id 2 and "is_online": false
And no error is returned
Scenario Outline: Publishing or unpublishing an unknown article answers 404
Given a valid jwt cookie for a user with id 1
And the article repository has no article with id 9999
When the client sends PATCH /v1/article/9999/<action> with that cookie
Then the response status is 404
And the response body is the text "Article not found"
Examples:
| action |
| publish |
| unpublish |
Scenario Outline: A malformed id answers 400 before any repository call
Given a valid jwt cookie for a user with id 1
When the client sends PATCH /v1/article/not-a-number/<action> with that cookie
Then the response status is 400
And the response body is the text apperr.IdUint32ErrorMessage
Examples:
| action |
| publish |
| unpublish |
Scenario Outline: Publish and unpublish require authentication
Given no jwt cookie is sent
And the article repository contains an article with id 1
When the client sends PATCH /v1/article/1/<action> without a cookie
Then the response status is 401
Examples:
| action |
| publish |
| unpublish |
Scenario: Publishing an article makes it appear in the public online-only list
Given a valid jwt cookie for a user with id 1
And the article repository contains an offline article titled "Draft: Kitchen Remodel" with id 2
When the client sends PATCH /v1/article/2/publish with that cookie
And the client then sends GET /v1/article/ with no jwt cookie
Then the response status of the second request is 200
And the response body of the second request is a JSON array containing the article with id 2
Scenario: Unpublishing an article removes it from the public online-only list
Given a valid jwt cookie for a user with id 1
And the article repository contains an online article titled "Backyard Renovation Update" with id 1
When the client sends PATCH /v1/article/1/unpublish with that cookie
And the client then sends GET /v1/article/ with no jwt cookie
Then the response status of the second request is 200
And the article with id 1 is absent from the response body of the second request
+57
View File
@@ -0,0 +1,57 @@
# QA procedure: article_publishing
Package: `test` (black-box, extending `article_test.go`).
## Setup shared by every case
1. Call `validator.InitValidator()` once.
2. Reuse `mockArticleRepo` (`test/article_test.go`) as-is; its `UpdateIsOnline` and `FindByFilters`
already support everything this slice needs, no fake changes expected.
3. Wire the real use cases over the fake: `useCases.NewCreateArticleUseCase`,
`useCases.NewGetArticleUseCase`, `useCases.NewListArticlesUseCase`,
`useCases.NewDeleteArticleUseCase`, `useCases.NewUpdateArticleUseCase`. Build
`article.NewHandler(...)` extended to also take the update use case (matching whatever
`NewHandler` signature the coder lands on), exposing the new `PublishArticle` and
`UnpublishArticle` methods.
4. Build a test `jwtauth.New("HS256", []byte("test-secret"), nil)`.
5. Mount the routes on a fresh `chi.NewRouter()` mirroring `api/router.go`'s current shape
(per `public_article_listing.qa.md`): `GET /` under `jwtauth.Verifier` only; `GET /{id}`,
`POST /`, `DELETE /{id}` under `jwtauth.Verifier` + `jwtauth.Authenticator`. Add
`PATCH /{id}/publish` and `PATCH /{id}/unpublish` to that same authenticated group.
6. Mint a valid jwt cookie for user id 1: `token.Encode(map[string]any{"user_id": uint32(1)})`,
set as the `jwt` cookie on the relevant `httptest.NewRequest`.
7. Seed the repo directly via the map pattern already used elsewhere, e.g.
`repo.articles[1] = article.Article{ID: 1, Title: "Backyard Renovation Update", IsOnline: true}`
and `repo.articles[2] = article.Article{ID: 2, Title: "Draft: Kitchen Remodel", IsOnline: false}`.
## HTTP checks
8. `PATCH /v1/article/2/publish` with the valid cookie, repo seeded per step 7 -> status `200`;
decode the JSON body and assert `id == 2` and `is_online == true`; assert `repo.articles[2].IsOnline`
is now `true`.
9. `PATCH /v1/article/1/unpublish` with the valid cookie, repo seeded per step 7 -> status `200`;
decode the JSON body and assert `id == 1` and `is_online == false`; assert
`repo.articles[1].IsOnline` is now `false`.
10. `PATCH /v1/article/1/publish` with the valid cookie against an article already seeded
`IsOnline: true` -> status `200`, body `is_online == true`, no error (idempotency).
11. `PATCH /v1/article/2/unpublish` with the valid cookie against an article already seeded
`IsOnline: false` -> status `200`, body `is_online == false`, no error (idempotency).
12. `PATCH /v1/article/9999/publish` with the valid cookie, empty repo id `9999` -> status `404`,
body `"Article not found\n"`. Repeat for `PATCH /v1/article/9999/unpublish` -> same assertions.
13. `PATCH /v1/article/not-a-number/publish` with the valid cookie -> status `400`, body matching
`apperr.IdUint32ErrorMessage + "\n"`. Repeat for `.../unpublish`.
14. `PATCH /v1/article/1/publish` with **no** cookie, repo seeded with article id 1 -> status `401`.
Repeat for `PATCH /v1/article/1/unpublish` with no cookie -> status `401`.
15. Integration with the public listing: seed repo with an offline article id 2, call
`PATCH /v1/article/2/publish` with the valid cookie (assert `200`), then call
`GET /v1/article/` with no cookie and assert the decoded JSON array contains an element with
id `2`.
16. Mirror check: seed repo with an online article id 1, call `PATCH /v1/article/1/unpublish` with
the valid cookie (assert `200`), then call `GET /v1/article/` with no cookie and assert the
decoded JSON array does not contain an element with id `1`.
## Manual verification (not exercised by the Go test)
- SPA-visible behaviour: confirm the back-office article editor exposes a publish/unpublish
control that calls the new endpoints and updates the visible online/offline state, against the
real embedded frontend and a real SQLite file. Not exercised by `task test`; requires `task dev`
or a built `task build` binary. (No such UI exists yet as of this slice — this note applies once
the SPA is wired to the new endpoints.)
- No SMTP involvement.
+317
View File
@@ -0,0 +1,317 @@
package test
import (
"RenewCMS/api/handlers/article"
"RenewCMS/api/pkg/apperr"
validator "RenewCMS/api/pkg/validator"
domainArticle "RenewCMS/internal/domain/article"
"RenewCMS/internal/infrastructure/useCases"
"bytes"
"encoding/json"
"log"
"net/http"
"net/http/httptest"
"testing"
"github.com/go-chi/chi/v5"
"github.com/go-chi/jwtauth/v5"
)
func newArticlePublishingRouter(articleRepo *mockArticleRepo, token *jwtauth.JWTAuth) *chi.Mux {
articleHandler := article.NewHandler(
useCases.NewCreateArticleUseCase(articleRepo),
useCases.NewGetArticleUseCase(articleRepo),
useCases.NewListArticlesUseCase(articleRepo),
useCases.NewDeleteArticleUseCase(articleRepo),
useCases.NewUpdateArticleUseCase(articleRepo),
)
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.Get("/", articleHandler.ListArticles)
r.Group(func(r chi.Router) {
r.Use(jwtauth.Authenticator(token))
r.Get("/{id}", articleHandler.GetArticle)
r.Post("/", articleHandler.PostArticle)
r.Delete("/{id}", articleHandler.DeleteArticle)
r.Patch("/{id}/publish", articleHandler.PublishArticle)
r.Patch("/{id}/unpublish", articleHandler.UnpublishArticle)
})
})
})
})
return r
}
func newPublishingTestArticleRepo() *mockArticleRepo {
repo := newMockArticleRepo()
repo.articles[1] = domainArticle.Article{ID: 1, Title: "Backyard Renovation Update", IsOnline: true}
repo.articles[2] = domainArticle.Article{ID: 2, Title: "Draft: Kitchen Remodel", IsOnline: false}
return repo
}
func setPublishingJwtCookie(t *testing.T, req *http.Request, token *jwtauth.JWTAuth) {
t.Helper()
_, tokenString, err := token.Encode(map[string]any{"user_id": uint32(1)})
if err != nil {
t.Fatalf("failed to encode jwt: %v", err)
}
req.AddCookie(&http.Cookie{Name: "jwt", Value: tokenString})
}
func TestArticlePublishingHttp(t *testing.T) {
validator.InitValidator()
token := jwtauth.New("HS256", []byte("test-secret"), nil)
t.Run("Publishing an offline article makes it online", func(t *testing.T) {
repo := newPublishingTestArticleRepo()
router := newArticlePublishingRouter(repo, token)
req := httptest.NewRequest(http.MethodPatch, "/v1/article/2/publish", nil)
setPublishingJwtCookie(t, req, token)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected status 200, got %d", rec.Code)
}
var got domainArticle.Article
if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil {
t.Fatalf("failed to decode response: %v", err)
}
if got.ID != 2 || !got.IsOnline {
t.Errorf("expected article 2 online, got %+v", got)
}
if !repo.articles[2].IsOnline {
t.Errorf("expected repository article 2 to be online")
}
})
t.Run("Unpublishing an online article makes it offline", func(t *testing.T) {
repo := newPublishingTestArticleRepo()
router := newArticlePublishingRouter(repo, token)
req := httptest.NewRequest(http.MethodPatch, "/v1/article/1/unpublish", nil)
setPublishingJwtCookie(t, req, token)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected status 200, got %d", rec.Code)
}
var got domainArticle.Article
if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil {
t.Fatalf("failed to decode response: %v", err)
}
if got.ID != 1 || got.IsOnline {
t.Errorf("expected article 1 offline, got %+v", got)
}
if repo.articles[1].IsOnline {
t.Errorf("expected repository article 1 to be offline")
}
})
t.Run("Publishing an already-online article is idempotent", func(t *testing.T) {
repo := newPublishingTestArticleRepo()
router := newArticlePublishingRouter(repo, token)
req := httptest.NewRequest(http.MethodPatch, "/v1/article/1/publish", nil)
setPublishingJwtCookie(t, req, token)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected status 200, got %d", rec.Code)
}
var got domainArticle.Article
if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil {
t.Fatalf("failed to decode response: %v", err)
}
if got.ID != 1 || !got.IsOnline {
t.Errorf("expected article 1 online, got %+v", got)
}
})
t.Run("Unpublishing an already-offline article is idempotent", func(t *testing.T) {
repo := newPublishingTestArticleRepo()
router := newArticlePublishingRouter(repo, token)
req := httptest.NewRequest(http.MethodPatch, "/v1/article/2/unpublish", nil)
setPublishingJwtCookie(t, req, token)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected status 200, got %d", rec.Code)
}
var got domainArticle.Article
if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil {
t.Fatalf("failed to decode response: %v", err)
}
if got.ID != 2 || got.IsOnline {
t.Errorf("expected article 2 offline, got %+v", got)
}
})
notFoundCases := []struct {
name string
action string
}{
{"publish", "publish"},
{"unpublish", "unpublish"},
}
for _, tc := range notFoundCases {
t.Run("Publishing or unpublishing an unknown article answers 404: "+tc.name, func(t *testing.T) {
repo := newPublishingTestArticleRepo()
router := newArticlePublishingRouter(repo, token)
req := httptest.NewRequest(http.MethodPatch, "/v1/article/9999/"+tc.action, nil)
setPublishingJwtCookie(t, req, token)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
if rec.Code != http.StatusNotFound {
t.Fatalf("expected status 404, got %d", rec.Code)
}
if rec.Body.String() != apperr.ArticleNotFoundErrorMessage+"\n" {
t.Errorf("unexpected body: %q", rec.Body.String())
}
})
}
for _, tc := range notFoundCases {
t.Run("A malformed id answers 400 before any repository call: "+tc.name, func(t *testing.T) {
repo := newPublishingTestArticleRepo()
router := newArticlePublishingRouter(repo, token)
req := httptest.NewRequest(http.MethodPatch, "/v1/article/not-a-number/"+tc.action, nil)
setPublishingJwtCookie(t, req, token)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
if rec.Code != http.StatusBadRequest {
t.Fatalf("expected status 400, got %d", rec.Code)
}
if rec.Body.String() != apperr.IdUint32ErrorMessage+"\n" {
t.Errorf("unexpected body: %q", rec.Body.String())
}
})
}
for _, tc := range notFoundCases {
t.Run("Publish and unpublish require authentication: "+tc.action, func(t *testing.T) {
repo := newPublishingTestArticleRepo()
router := newArticlePublishingRouter(repo, token)
req := httptest.NewRequest(http.MethodPatch, "/v1/article/1/"+tc.action, nil)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
if rec.Code != http.StatusUnauthorized {
t.Fatalf("expected status 401, got %d", rec.Code)
}
})
}
t.Run("Publishing an article makes it appear in the public online-only list", func(t *testing.T) {
repo := newPublishingTestArticleRepo()
router := newArticlePublishingRouter(repo, token)
req := httptest.NewRequest(http.MethodPatch, "/v1/article/2/publish", nil)
setPublishingJwtCookie(t, req, token)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected status 200, got %d", rec.Code)
}
listReq := httptest.NewRequest(http.MethodGet, "/v1/article/", nil)
listRec := httptest.NewRecorder()
router.ServeHTTP(listRec, listReq)
if listRec.Code != http.StatusOK {
t.Fatalf("expected status 200, got %d", listRec.Code)
}
var articles []domainArticle.Article
if err := json.Unmarshal(listRec.Body.Bytes(), &articles); err != nil {
t.Fatalf("failed to decode response: %v", err)
}
found := false
for _, a := range articles {
if a.ID == 2 {
found = true
}
}
if !found {
t.Errorf("expected article 2 in the public listing, got %+v", articles)
}
})
t.Run("Publishing an article does not log a write failure on success", func(t *testing.T) {
repo := newPublishingTestArticleRepo()
router := newArticlePublishingRouter(repo, token)
req := httptest.NewRequest(http.MethodPatch, "/v1/article/2/publish", nil)
setPublishingJwtCookie(t, req, token)
rec := httptest.NewRecorder()
var logBuf bytes.Buffer
previousOutput := log.Writer()
log.SetOutput(&logBuf)
defer log.SetOutput(previousOutput)
router.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected status 200, got %d", rec.Code)
}
if logBuf.Len() != 0 {
t.Errorf("expected no write-failure log, got %q", logBuf.String())
}
})
t.Run("A repository failure answers 500", func(t *testing.T) {
repo := newPublishingTestArticleRepo()
repo.fail.failNext("UpdateIsOnline")
router := newArticlePublishingRouter(repo, token)
req := httptest.NewRequest(http.MethodPatch, "/v1/article/1/publish", nil)
setPublishingJwtCookie(t, req, token)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
if rec.Code != http.StatusInternalServerError {
t.Fatalf("expected status 500, got %d", rec.Code)
}
})
t.Run("Unpublishing an article removes it from the public online-only list", func(t *testing.T) {
repo := newPublishingTestArticleRepo()
repo.articles[3] = domainArticle.Article{ID: 3, Title: "Still Online Article", IsOnline: true}
router := newArticlePublishingRouter(repo, token)
req := httptest.NewRequest(http.MethodPatch, "/v1/article/1/unpublish", nil)
setPublishingJwtCookie(t, req, token)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected status 200, got %d", rec.Code)
}
listReq := httptest.NewRequest(http.MethodGet, "/v1/article/", nil)
listRec := httptest.NewRecorder()
router.ServeHTTP(listRec, listReq)
if listRec.Code != http.StatusOK {
t.Fatalf("expected status 200, got %d", listRec.Code)
}
var articles []domainArticle.Article
if err := json.Unmarshal(listRec.Body.Bytes(), &articles); err != nil {
t.Fatalf("failed to decode response: %v", err)
}
foundStillOnline := false
for _, a := range articles {
if a.ID == 1 {
t.Errorf("expected article 1 absent from the public listing, got %+v", articles)
}
if a.ID == 3 {
foundStillOnline = true
}
}
if !foundStillOnline {
t.Errorf("expected article 3 present in the public listing, got %+v", articles)
}
})
}
+1
View File
@@ -26,6 +26,7 @@ func newArticleTitleRulesRouter(repo *mockArticleRepo, token *jwtauth.JWTAuth) *
useCases.NewGetArticleUseCase(repo),
useCases.NewListArticlesUseCase(repo),
useCases.NewDeleteArticleUseCase(repo),
useCases.NewUpdateArticleUseCase(repo),
)
r := chi.NewRouter()
+1
View File
@@ -129,6 +129,7 @@ func newNotFoundErrorsRouter(articleRepo *mockArticleRepo, userRepo *mockUserRep
useCases.NewGetArticleUseCase(articleRepo),
useCases.NewListArticlesUseCase(articleRepo),
useCases.NewDeleteArticleUseCase(articleRepo),
useCases.NewUpdateArticleUseCase(articleRepo),
)
hasher := hashing.NewBcryptHasher()
authHandler := auth.NewHandler(
+1
View File
@@ -42,6 +42,7 @@ func newPublicArticleListingRouter(articleRepo *mockArticleRepo, token *jwtauth.
useCases.NewGetArticleUseCase(articleRepo),
useCases.NewListArticlesUseCase(articleRepo),
useCases.NewDeleteArticleUseCase(articleRepo),
useCases.NewUpdateArticleUseCase(articleRepo),
)
r := chi.NewRouter()
+1
View File
@@ -193,6 +193,7 @@ func newDeleteArticleRouter(articleRepo *mockArticleRepo, token *jwtauth.JWTAuth
useCases.NewGetArticleUseCase(articleRepo),
useCases.NewListArticlesUseCase(articleRepo),
useCases.NewDeleteArticleUseCase(articleRepo),
useCases.NewUpdateArticleUseCase(articleRepo),
)
r := chi.NewRouter()