diff --git a/implem/dummy.articleValidator/validator.go b/implem/dummy.articleValidator/validator.go new file mode 100644 index 0000000..880f2aa --- /dev/null +++ b/implem/dummy.articleValidator/validator.go @@ -0,0 +1,16 @@ +package articleValidator + +import ( + "github.com/err0r500/go-realworld-clean/domain" + "github.com/err0r500/go-realworld-clean/uc" +) + +type validator struct { +} + +func New() uc.ArticleValidator { + return validator{} +} + +func (validator) BeforeCreationCheck(article *domain.Article) error { return nil } +func (validator) BeforeUpdateCheck(article *domain.Article) error { return nil } diff --git a/implem/gin.server/articles_test.go b/implem/gin.server/articles_test.go index 57b6053..810d36a 100644 --- a/implem/gin.server/articles_test.go +++ b/implem/gin.server/articles_test.go @@ -57,31 +57,32 @@ func TestArticlesFiltered(t *testing.T) { } func TestArticlesFeed(t *testing.T) { - mockCtrl := gomock.NewController(t) - defer mockCtrl.Finish() + t.Run("happyCase", func(t *testing.T) { - limit := 10 - offset := 2 + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() - jane := testData.User("jane") + limit := 10 + offset := 2 - ucHandler := mock.NewMockHandler(mockCtrl) - ucHandler.EXPECT(). - ArticlesFeed(jane.Name, limit, offset). - Return(domain.ArticleCollection{testData.Article("jane")}, 10, nil). - Times(1) + jane := testData.User("jane") - jwtHandler := jwt.NewTokenHandler("mySalt") + ucHandler := mock.NewMockHandler(mockCtrl) + ucHandler.EXPECT(). + ArticlesFeed(jane.Name, limit, offset). + Return(domain.ArticleCollection{testData.Article("jane")}, 10, nil). + Times(1) - gE := gin.Default() - server.NewRouter(ucHandler, jwtHandler).SetRoutes(gE) - ts := httptest.NewServer(gE) - defer ts.Close() + jwtHandler := jwt.NewTokenHandler("mySalt") - authToken, err := jwtHandler.GenUserToken(jane.Name) - assert.NoError(t, err) + gE := gin.Default() + server.NewRouter(ucHandler, jwtHandler).SetRoutes(gE) + ts := httptest.NewServer(gE) + defer ts.Close() + + authToken, err := jwtHandler.GenUserToken(jane.Name) + assert.NoError(t, err) - t.Run("happyCase", func(t *testing.T) { baloo.New(ts.URL). Get(articlesFeedPath). AddHeader("Authorization", authToken). @@ -92,4 +93,41 @@ func TestArticlesFeed(t *testing.T) { JSONSchema(testData.ArticleMultipleRespDefinition). Done() }) + + t.Run("empty", func(t *testing.T) { + + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + limit := 10 + offset := 2 + + jane := testData.User("jane") + + ucHandler := mock.NewMockHandler(mockCtrl) + ucHandler.EXPECT(). + ArticlesFeed(jane.Name, limit, offset). + Return(nil, 0, nil). + Times(1) + + jwtHandler := jwt.NewTokenHandler("mySalt") + + gE := gin.Default() + server.NewRouter(ucHandler, jwtHandler).SetRoutes(gE) + ts := httptest.NewServer(gE) + defer ts.Close() + + authToken, err := jwtHandler.GenUserToken(jane.Name) + assert.NoError(t, err) + + baloo.New(ts.URL). + Get(articlesFeedPath). + AddHeader("Authorization", authToken). + AddQuery("limit", strconv.Itoa(limit)). + AddQuery("offset", strconv.Itoa(offset)). + Expect(t). + Status(200). + BodyEquals(`{"articles":[],"articlesCount":0}`). + Done() + }) } diff --git a/implem/gin.server/tagsGet.go b/implem/gin.server/tagsGet.go index d10b16a..d829cd8 100644 --- a/implem/gin.server/tagsGet.go +++ b/implem/gin.server/tagsGet.go @@ -16,5 +16,8 @@ func (rH RouterHandler) tagsGet(c *gin.Context) { return } + if tags == nil { + tags = []string{} + } c.JSON(http.StatusOK, gin.H{"tags": tags}) } diff --git a/implem/gin.server/tagsGet_test.go b/implem/gin.server/tagsGet_test.go index f6bdc5a..0dcd8c9 100644 --- a/implem/gin.server/tagsGet_test.go +++ b/implem/gin.server/tagsGet_test.go @@ -18,28 +18,54 @@ import ( var tagsPath = "/api/tags" func TestTagsGet_happyCase(t *testing.T) { - mockCtrl := gomock.NewController(t) - defer mockCtrl.Finish() + t.Run("simple", func(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() - tags := []string{"tag1", "tag2"} - ucHandler := uc.NewMockHandler(mockCtrl) - ucHandler.EXPECT(). - Tags(). - Return(tags, nil). - Times(1) + tags := []string{"tag1", "tag2"} + ucHandler := uc.NewMockHandler(mockCtrl) + ucHandler.EXPECT(). + Tags(). + Return(tags, nil). + Times(1) - gE := gin.Default() - server.NewRouter(ucHandler, nil).SetRoutes(gE) + gE := gin.Default() + server.NewRouter(ucHandler, nil).SetRoutes(gE) - ts := httptest.NewServer(gE) - defer ts.Close() + ts := httptest.NewServer(gE) + defer ts.Close() + + baloo.New(ts.URL). + Get(tagsPath). + Expect(t). + Status(http.StatusOK). + JSONSchema(testData.TagsResponse). + Done() + }) + t.Run("empty", func(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + ucHandler := uc.NewMockHandler(mockCtrl) + ucHandler.EXPECT(). + Tags(). + Return(nil, nil). + Times(1) + + gE := gin.Default() + server.NewRouter(ucHandler, nil).SetRoutes(gE) + + ts := httptest.NewServer(gE) + defer ts.Close() + + baloo.New(ts.URL). + Get(tagsPath). + Expect(t). + Status(http.StatusOK). + BodyEquals(`{"tags":[]}`). + Done() + }) - baloo.New(ts.URL). - Get(tagsPath). - Expect(t). - Status(http.StatusOK). - JSONSchema(testData.TagsResponse). - Done() } func TestTagsGet_fail(t *testing.T) { diff --git a/implem/gosimple.slugger/slugger.go b/implem/gosimple.slugger/slugger.go new file mode 100644 index 0000000..4282adb --- /dev/null +++ b/implem/gosimple.slugger/slugger.go @@ -0,0 +1,16 @@ +package slugger + +import ( + "github.com/err0r500/go-realworld-clean/uc" + "github.com/gosimple/slug" +) + +type slugger struct{} + +func New() uc.Slugger { + return slugger{} +} + +func (slugger) NewSlug(initial string) string { + return slug.Make(initial) +} diff --git a/implem/json.formatter/article.go b/implem/json.formatter/article.go index 1f9a7b6..dab38df 100644 --- a/implem/json.formatter/article.go +++ b/implem/json.formatter/article.go @@ -33,7 +33,7 @@ func NewArticleFromDomain(article domain.Article) Article { } func NewArticlesFromDomain(articles ...domain.Article) []Article { - var ret []Article + ret := []Article{} for _, article := range articles { ret = append(ret, NewArticleFromDomain(article)) } diff --git a/implem/memory.articleRW/readWriter.go b/implem/memory.articleRW/readWriter.go index 04b52dc..055baf4 100644 --- a/implem/memory.articleRW/readWriter.go +++ b/implem/memory.articleRW/readWriter.go @@ -3,6 +3,8 @@ package articleRW import ( "sync" + "errors" + "github.com/err0r500/go-realworld-clean/domain" "github.com/err0r500/go-realworld-clean/uc" ) @@ -16,14 +18,67 @@ func New() uc.ArticleRW { store: &sync.Map{}, } } +func (rw rw) Create(article domain.Article) (*domain.Article, error) { + if _, err := rw.GetBySlug(article.Slug); err == nil { + return nil, uc.ErrAlreadyInUse + } + + rw.store.Store(article.Slug, article) + + return rw.GetBySlug(article.Slug) +} + +func (rw rw) GetByAuthorsNameOrderedByMostRecentAsc(usernames []string) ([]domain.Article, error) { + var toReturn []domain.Article + + rw.store.Range(func(key, value interface{}) bool { + article, ok := value.(domain.Article) + if !ok { + return true // log this but continue + } + for _, username := range usernames { + if article.Author.Name == username { + toReturn = append(toReturn, article) + } + } + return true + }) + + return toReturn, nil +} + +func (rw) GetRecentFiltered(filters uc.Filters) ([]domain.Article, error) { + // todo => check if its AND or OR filters -func (rw) GetByAuthorsNameOrderedByMostRecentAsc(usernames []string) ([]domain.Article, error) { return nil, nil } -func (rw) GetRecentFiltered(filters uc.Filters) ([]domain.Article, error) { return nil, nil } +func (rw rw) Save(article domain.Article) (*domain.Article, error) { + if _, err := rw.GetBySlug(article.Slug); err != nil { + return nil, uc.ErrNotFound + } + + rw.store.Store(article.Slug, article) + + return rw.GetBySlug(article.Slug) +} + +func (rw rw) GetBySlug(slug string) (*domain.Article, error) { + value, ok := rw.store.Load(slug) + if !ok { + return nil, uc.ErrNotFound + } -func (rw) Create(domain.Article) (*domain.Article, error) { return nil, nil } -func (rw) Save(domain.Article) (*domain.Article, error) { return nil, nil } -func (rw) GetBySlug(slug string) (*domain.Article, error) { return nil, nil } -func (rw) Delete(slug string) error { return nil } + article, ok := value.(domain.Article) + if !ok { + return nil, errors.New("not an article stored at key") + } + + return &article, nil +} + +func (rw rw) Delete(slug string) error { + rw.store.Delete(slug) + + return nil +} diff --git a/implem/memory.articleRW/readWriter_test.go b/implem/memory.articleRW/readWriter_test.go new file mode 100644 index 0000000..13a299e --- /dev/null +++ b/implem/memory.articleRW/readWriter_test.go @@ -0,0 +1 @@ +package articleRW diff --git a/implem/memory.commentRW/readWriter.go b/implem/memory.commentRW/readWriter.go new file mode 100644 index 0000000..565cda0 --- /dev/null +++ b/implem/memory.commentRW/readWriter.go @@ -0,0 +1,50 @@ +package commentRW + +import ( + "sync" + + "errors" + + "github.com/err0r500/go-realworld-clean/domain" + "github.com/err0r500/go-realworld-clean/uc" +) + +type rw struct { + store *sync.Map +} + +func New() uc.CommentRW { + return rw{ + store: &sync.Map{}, + } +} + +func (rw rw) Create(comment domain.Comment) (*domain.Comment, error) { + if _, err := rw.GetByID(comment.ID); err == nil { + return nil, uc.ErrAlreadyInUse + } + + rw.store.Store(comment.ID, comment) + + return rw.GetByID(comment.ID) +} + +func (rw rw) GetByID(id int) (*domain.Comment, error) { + value, ok := rw.store.Load(id) + if !ok { + return nil, uc.ErrNotFound + } + + comment, ok := value.(domain.Comment) + if !ok { + return nil, errors.New("not an article stored at key") + } + + return &comment, nil +} + +func (rw rw) Delete(id int) error { + rw.store.Delete(id) + + return nil +} diff --git a/implem/memory.commentRW/readWriter_test.go b/implem/memory.commentRW/readWriter_test.go new file mode 100644 index 0000000..1d8a924 --- /dev/null +++ b/implem/memory.commentRW/readWriter_test.go @@ -0,0 +1 @@ +package commentRW_test diff --git a/implem/memory.tagsRW/readWriter.go b/implem/memory.tagsRW/readWriter.go new file mode 100644 index 0000000..4b3704c --- /dev/null +++ b/implem/memory.tagsRW/readWriter.go @@ -0,0 +1,43 @@ +package tagsRW + +import ( + "sync" + + "github.com/err0r500/go-realworld-clean/uc" +) + +type rw struct { + store *sync.Map +} + +func New() uc.TagsRW { + return rw{ + store: &sync.Map{}, + } +} + +// lots of ways to improve this (use an array as cache, use index access instead of append...) +// no perf problem for now => no optimisation :) +func (rw rw) GetAll() ([]string, error) { + var toReturn []string + + rw.store.Range(func(key, value interface{}) bool { + tag, ok := key.(string) + if !ok { + return true + } + toReturn = append(toReturn, tag) + return true + }) + + return toReturn, nil +} + +func (rw rw) Add(newTags []string) error { + + for _, tag := range newTags { + rw.store.Store(tag, true) + } + + return nil +} diff --git a/implem/memory.tagsRW/readWriter_test.go b/implem/memory.tagsRW/readWriter_test.go new file mode 100644 index 0000000..8e6a100 --- /dev/null +++ b/implem/memory.tagsRW/readWriter_test.go @@ -0,0 +1 @@ +package tagsRW_test diff --git a/implem/memory.userRW/readWriter.go b/implem/memory.userRW/readWriter.go index 1194dbd..09bf9fd 100644 --- a/implem/memory.userRW/readWriter.go +++ b/implem/memory.userRW/readWriter.go @@ -21,7 +21,7 @@ func New() uc.UserRW { func (rw rw) Create(username, email, password string) (*domain.User, error) { if _, err := rw.GetByName(username); err == nil { - return nil, uc.ErrUserNameAlreadyInUse + return nil, uc.ErrAlreadyInUse } rw.store.Store(username, domain.User{ @@ -36,7 +36,7 @@ func (rw rw) Create(username, email, password string) (*domain.User, error) { func (rw rw) GetByName(userName string) (*domain.User, error) { value, ok := rw.store.Load(userName) if !ok { - return nil, uc.ErrUserNotFound + return nil, uc.ErrNotFound } user, ok := value.(domain.User) @@ -71,7 +71,7 @@ func (rw rw) GetByEmailAndPassword(email, password string) (*domain.User, error) func (rw rw) Save(user domain.User) error { if user, _ := rw.GetByName(user.Name); user == nil { - return uc.ErrUserNotFound + return uc.ErrNotFound } rw.store.Store(user.Name, user) diff --git a/main.go b/main.go index 3a4aea3..39b70dc 100644 --- a/main.go +++ b/main.go @@ -3,10 +3,13 @@ package main import ( "fmt" + "github.com/err0r500/go-realworld-clean/implem/dummy.articleValidator" "github.com/err0r500/go-realworld-clean/implem/gin.server" + "github.com/err0r500/go-realworld-clean/implem/gosimple.slugger" "github.com/err0r500/go-realworld-clean/implem/jwt.authHandler" "github.com/err0r500/go-realworld-clean/implem/logrus.logger" "github.com/err0r500/go-realworld-clean/implem/memory.articleRW" + "github.com/err0r500/go-realworld-clean/implem/memory.tagsRW" "github.com/err0r500/go-realworld-clean/implem/memory.userRW" "github.com/err0r500/go-realworld-clean/implem/user.validator" "github.com/err0r500/go-realworld-clean/infra" @@ -66,11 +69,14 @@ func run() { server.NewRouterWithLogger( uc.HandlerConstructor{ - Logger: routerLogger, - UserRW: userRW.New(), - ArticleRW: articleRW.New(), - UserValidator: validator.New(), - AuthHandler: authHandler, + Logger: routerLogger, + UserRW: userRW.New(), + ArticleRW: articleRW.New(), + UserValidator: validator.New(), + AuthHandler: authHandler, + Slugger: slugger.New(), + ArticleValidator: articleValidator.New(), + TagsRW: tagsRW.New(), }.New(), authHandler, routerLogger, diff --git a/uc/profileUpdateFollow.go b/uc/profileUpdateFollow.go index 8c90915..64e7da1 100644 --- a/uc/profileUpdateFollow.go +++ b/uc/profileUpdateFollow.go @@ -11,7 +11,7 @@ func (i interactor) ProfileUpdateFollow(userName, followeeName string, follow bo return nil, errWrongUser } if user == nil { - return nil, ErrUserNotFound + return nil, ErrNotFound } user.UpdateFollowees(followeeName, follow) diff --git a/uc/shared.go b/uc/shared.go index 3a183b1..27b8df6 100644 --- a/uc/shared.go +++ b/uc/shared.go @@ -3,10 +3,10 @@ package uc import "errors" var ( - ErrUserNameAlreadyInUse = errors.New("this username is already in use") + ErrAlreadyInUse = errors.New("this username is already in use") //ErrUserEmailAlreadyInUsed = errors.New("this email address is already in use") errWrongUser = errors.New("woops, wrong user") errProfileNotFound = errors.New("profile not found") - ErrUserNotFound = errors.New("user not found") + ErrNotFound = errors.New("user not found") errArticleNotFound = errors.New("article not found") ) diff --git a/uc/userEdit.go b/uc/userEdit.go index a0f3d8b..ff73770 100644 --- a/uc/userEdit.go +++ b/uc/userEdit.go @@ -24,7 +24,7 @@ func (i interactor) UserEdit(userName string, fieldsToUpdate map[UpdatableProper return nil, "", errWrongUser } if user == nil { - return nil, "", ErrUserNotFound + return nil, "", ErrNotFound } domain.UpdateUser(user, diff --git a/uc/userGet.go b/uc/userGet.go index 3d7c91f..2e947af 100644 --- a/uc/userGet.go +++ b/uc/userGet.go @@ -10,7 +10,7 @@ func (i interactor) UserGet(userName string) (*domain.User, string, error) { return nil, "", err } if user == nil { - return nil, "", ErrUserNotFound + return nil, "", ErrNotFound } if user.Name != userName { return nil, "", errWrongUser diff --git a/uc/userLogin.go b/uc/userLogin.go index a1d6b1f..aa23431 100644 --- a/uc/userLogin.go +++ b/uc/userLogin.go @@ -8,7 +8,7 @@ func (i interactor) UserLogin(email, password string) (*domain.User, string, err return nil, "", err } if user == nil { - return nil, "", ErrUserNotFound + return nil, "", ErrNotFound } token, err := i.authHandler.GenUserToken(user.Name)