diff --git a/domain/article.go b/domain/article.go index ff8552d..ed1ad7b 100644 --- a/domain/article.go +++ b/domain/article.go @@ -19,7 +19,7 @@ type Article struct { } type Comment struct { - ID string `json:"id"` + ID int `json:"id"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` Body string `json:"body"` @@ -50,3 +50,16 @@ func (articles ArticleCollection) ApplyLimitAndOffset(limit, offset int) Article return articles[min:max] } + +func (article *Article) UpdateComments(comment Comment, add bool) { + if add { + article.Comments = append(article.Comments, comment) + return + } + + for i := 0; i < len(article.Comments); i++ { + if article.Comments[i].ID == comment.ID { + article.Comments = append(article.Comments[:i], article.Comments[i+1:]...) // memory leak ? https://github.com/golang/go/wiki/SliceTricks + } + } +} diff --git a/implem/gin.server/articleComment.go b/implem/gin.server/articleComment.go index ab1ff46..e4341f3 100644 --- a/implem/gin.server/articleComment.go +++ b/implem/gin.server/articleComment.go @@ -3,6 +3,8 @@ package server import ( "net/http" + "strconv" + "github.com/err0r500/go-realworld-clean/implem/json.formatter" "github.com/gin-gonic/gin" ) @@ -47,8 +49,12 @@ func (rH RouterHandler) commentPost(c *gin.Context) { func (rH RouterHandler) commentDelete(c *gin.Context) { log := rH.log(rH.MethodAndPath(c)) - - if err := rH.ucHandler.CommentsDelete(rH.getUserName(c), c.Param("slug"), c.Param("id")); err != nil { + id, err := strconv.Atoi(c.Param("id")) + if err != nil { + c.Status(http.StatusBadRequest) + return + } + if err := rH.ucHandler.CommentsDelete(rH.getUserName(c), c.Param("slug"), id); err != nil { log(err) c.Status(http.StatusUnprocessableEntity) return diff --git a/implem/gin.server/articleComment_test.go b/implem/gin.server/articleComment_test.go index 8916e07..ed04c41 100644 --- a/implem/gin.server/articleComment_test.go +++ b/implem/gin.server/articleComment_test.go @@ -4,6 +4,8 @@ import ( "net/http/httptest" "testing" + "strconv" + "github.com/err0r500/go-realworld-clean/implem/gin.server" "github.com/err0r500/go-realworld-clean/implem/jwt.authHandler" "github.com/err0r500/go-realworld-clean/implem/mock.uc" @@ -138,7 +140,7 @@ func TestArticleCommentDelete(t *testing.T) { t.Run("happyCase", func(t *testing.T) { baloo.New(ts.URL). - Delete(articleCommentPath+"/"+testData.Article("jane").Comments[0].ID). + Delete(articleCommentPath+"/"+strconv.Itoa(testData.Article("jane").Comments[0].ID)). AddHeader("Authorization", authToken). Expect(t). Status(200). @@ -147,7 +149,7 @@ func TestArticleCommentDelete(t *testing.T) { t.Run("no auth", func(t *testing.T) { baloo.New(ts.URL). - Delete(articleCommentPath + "/" + testData.Article("jane").Comments[0].ID). + Delete(articleCommentPath + "/" + strconv.Itoa(testData.Article("jane").Comments[0].ID)). Expect(t). Status(401). Done() diff --git a/implem/json.formatter/comment.go b/implem/json.formatter/comment.go index 4b2cd94..62c7f0d 100644 --- a/implem/json.formatter/comment.go +++ b/implem/json.formatter/comment.go @@ -5,7 +5,7 @@ import ( ) type Comment struct { - ID string `json:"id"` + ID int `json:"id"` CreatedAt string `json:"createdAt"` UpdatedAt string `json:"updatedAt"` Body string `json:"body"` diff --git a/implem/mock.uc/handler.go b/implem/mock.uc/handler.go index fd1e934..c77357d 100644 --- a/implem/mock.uc/handler.go +++ b/implem/mock.uc/handler.go @@ -222,7 +222,7 @@ func (mr *MockHandlerMockRecorder) CommentsPost(username, slug, comment interfac } // CommentsDelete mocks base method -func (m *MockHandler) CommentsDelete(username, slug, id string) error { +func (m *MockHandler) CommentsDelete(username, slug string, id int) error { ret := m.ctrl.Call(m, "CommentsDelete", username, slug, id) ret0, _ := ret[0].(error) return ret0 @@ -562,7 +562,7 @@ func (mr *MockCommentsLogicMockRecorder) CommentsPost(username, slug, comment in } // CommentsDelete mocks base method -func (m *MockCommentsLogic) CommentsDelete(username, slug, id string) error { +func (m *MockCommentsLogic) CommentsDelete(username, slug string, id int) error { ret := m.ctrl.Call(m, "CommentsDelete", username, slug, id) ret0, _ := ret[0].(error) return ret0 diff --git a/implem/mock.uc/interactor.go b/implem/mock.uc/interactor.go index f9db803..681010f 100644 --- a/implem/mock.uc/interactor.go +++ b/implem/mock.uc/interactor.go @@ -271,6 +271,67 @@ func (mr *MockArticleRWMockRecorder) Delete(slug interface{}) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockArticleRW)(nil).Delete), slug) } +// MockCommentRW is a mock of CommentRW interface +type MockCommentRW struct { + ctrl *gomock.Controller + recorder *MockCommentRWMockRecorder +} + +// MockCommentRWMockRecorder is the mock recorder for MockCommentRW +type MockCommentRWMockRecorder struct { + mock *MockCommentRW +} + +// NewMockCommentRW creates a new mock instance +func NewMockCommentRW(ctrl *gomock.Controller) *MockCommentRW { + mock := &MockCommentRW{ctrl: ctrl} + mock.recorder = &MockCommentRWMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockCommentRW) EXPECT() *MockCommentRWMockRecorder { + return m.recorder +} + +// Create mocks base method +func (m *MockCommentRW) Create(comment domain.Comment) (*domain.Comment, error) { + ret := m.ctrl.Call(m, "Create", comment) + ret0, _ := ret[0].(*domain.Comment) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Create indicates an expected call of Create +func (mr *MockCommentRWMockRecorder) Create(comment interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockCommentRW)(nil).Create), comment) +} + +// GetByID mocks base method +func (m *MockCommentRW) GetByID(id int) (*domain.Comment, error) { + ret := m.ctrl.Call(m, "GetByID", id) + ret0, _ := ret[0].(*domain.Comment) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetByID indicates an expected call of GetByID +func (mr *MockCommentRWMockRecorder) GetByID(id interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetByID", reflect.TypeOf((*MockCommentRW)(nil).GetByID), id) +} + +// Delete mocks base method +func (m *MockCommentRW) Delete(id int) error { + ret := m.ctrl.Call(m, "Delete", id) + ret0, _ := ret[0].(error) + return ret0 +} + +// Delete indicates an expected call of Delete +func (mr *MockCommentRWMockRecorder) Delete(id interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockCommentRW)(nil).Delete), id) +} + // MockSlugger is a mock of Slugger interface type MockSlugger struct { ctrl *gomock.Controller diff --git a/testData/example.go b/testData/example.go index 5a18942..a3de976 100644 --- a/testData/example.go +++ b/testData/example.go @@ -68,7 +68,7 @@ var janeArticle = domain.Article{ FavoritesCount: 123, Author: domain.Profile{User: jane, Following: false}, Comments: []domain.Comment{ - {ID: "1", + {ID: 123, CreatedAt: time.Now(), UpdatedAt: time.Now(), Body: "commentBody", diff --git a/testData/jsonShemas.go b/testData/jsonShemas.go index ffcd6e4..349faf3 100644 --- a/testData/jsonShemas.go +++ b/testData/jsonShemas.go @@ -169,14 +169,13 @@ var ArticleMultipleRespDefinition = `{ ] }` -//fixme : id is integer in API contract var CommentDefinition = ` ` + ProfileDefinition + `, "Comment": { "type": "object", "properties": { "id": { - "type": "string" + "type": "integer" }, "createdAt": { "type": "string", diff --git a/uc/HANDLER.go b/uc/HANDLER.go index 1e0c868..bc588d9 100644 --- a/uc/HANDLER.go +++ b/uc/HANDLER.go @@ -43,7 +43,7 @@ type ArticleLogic interface { type CommentsLogic interface { CommentsGet(slug string) ([]domain.Comment, error) CommentsPost(username, slug, comment string) (*domain.Comment, error) - CommentsDelete(username, slug, id string) error + CommentsDelete(username, slug string, id int) error } type FavoritesLogic interface { diff --git a/uc/INTERACTOR.go b/uc/INTERACTOR.go index a93251f..1d03471 100644 --- a/uc/INTERACTOR.go +++ b/uc/INTERACTOR.go @@ -14,6 +14,7 @@ type interactor struct { articleValidator ArticleValidator authHandler AuthHandler slugger Slugger + commentRW CommentRW } // Logger : only used to log stuff @@ -42,6 +43,12 @@ type ArticleRW interface { Delete(slug string) error } +type CommentRW interface { + Create(comment domain.Comment) (*domain.Comment, error) + GetByID(id int) (*domain.Comment, error) + Delete(id int) error +} + type Slugger interface { NewSlug(string) string } diff --git a/uc/articlesRecent_test.go b/uc/articlesRecent_test.go new file mode 100644 index 0000000..2b5144b --- /dev/null +++ b/uc/articlesRecent_test.go @@ -0,0 +1,30 @@ +package uc_test + +import ( + "testing" + + "github.com/err0r500/go-realworld-clean/domain" + mock "github.com/err0r500/go-realworld-clean/implem/mock.uc" + "github.com/err0r500/go-realworld-clean/uc" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" +) + +func TestInteractor_GetArticles(t *testing.T) { + expectedArticles := domain.ArticleCollection{art1, art2, art3, art4} + + t.Run("most obvious", func(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + offset := 2 + filters := uc.NewFilters("jane", "", "") + i := mock.NewMockedInteractor(mockCtrl) + i.ArticleRW.EXPECT().GetRecentFiltered(filters).Return(expectedArticles, nil).Times(1) + + articles, count, err := i.GetUCHandler().GetArticles(10, offset, filters) + assert.NoError(t, err) + assert.Equal(t, 4, count) + assert.Equal(t, expectedArticles[offset:], articles) + }) +} diff --git a/uc/comments.go b/uc/comments.go index 61e1074..c9a20f7 100644 --- a/uc/comments.go +++ b/uc/comments.go @@ -1,15 +1,71 @@ package uc -import "github.com/err0r500/go-realworld-clean/domain" +import ( + "github.com/err0r500/go-realworld-clean/domain" +) func (i interactor) CommentsGet(slug string) ([]domain.Comment, error) { - return nil, nil + article, err := i.articleRW.GetBySlug(slug) + if err != nil { + return nil, err + } + + return article.Comments, nil } func (i interactor) CommentsPost(username, slug, comment string) (*domain.Comment, error) { - return nil, nil + commentPoster, err := i.userRW.GetByName(username) + if err != nil { + return nil, err + } + + article, err := i.articleRW.GetBySlug(slug) + if err != nil { + return nil, err + } + + rawComment := domain.Comment{ + Body: comment, + Author: domain.Profile{User: *commentPoster}, + } + + insertedComment, err := i.commentRW.Create(rawComment) + if err != nil { + return nil, err + } + + article.Comments = append(article.Comments, *insertedComment) + + if _, err := i.articleRW.Save(*article); err != nil { + return nil, err + } + + return insertedComment, nil } -func (i interactor) CommentsDelete(username string, slug, id string) error { +func (i interactor) CommentsDelete(username, slug string, id int) error { + comment, err := i.commentRW.GetByID(id) + if err != nil { + return err + } + if comment.Author.Name != username { + return errWrongUser + } + + if err := i.commentRW.Delete(id); err != nil { + return err + } + + article, err := i.articleRW.GetBySlug(slug) + if err != nil { + return err + } + + article.UpdateComments(*comment, false) + + if _, err := i.articleRW.Save(*article); err != nil { + return err + } + return nil }