diff --git a/.gitignore b/.gitignore index e660fd9..6df9c8e 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ bin/ +.idea/ diff --git a/Makefile b/Makefile index 68c5479..6320955 100644 --- a/Makefile +++ b/Makefile @@ -32,9 +32,9 @@ ci: dependencies test build-mocks: @go get github.com/golang/mock/gomock @go install github.com/golang/mock/mockgen - @~/go/bin/mockgen -source=domain/entity/book/interface.go -destination=domain/entity/book/mock/book.go -package=mock - @~/go/bin/mockgen -source=domain/entity/user/interface.go -destination=domain/entity/user/mock/user.go -package=mock - @~/go/bin/mockgen -source=domain/usecase/loan/interface.go -destination=domain/usecase/loan/mock/loan.go -package=mock + @~/go/bin/mockgen -source=usecase/book/interface.go -destination=usecase/book/mock/book.go -package=mock + @~/go/bin/mockgen -source=usecase/user/interface.go -destination=usecase/user/mock/user.go -package=mock + @~/go/bin/mockgen -source=usecase/loan/interface.go -destination=usecase/loan/mock/loan.go -package=mock test: go test -tags testing ./... diff --git a/api/handler/book.go b/api/handler/book.go index a3947ad..f6dc6f8 100644 --- a/api/handler/book.go +++ b/api/handler/book.go @@ -4,33 +4,30 @@ import ( "encoding/json" "log" "net/http" - "time" - "github.com/eminetto/clean-architecture-go-v2/domain" + "github.com/eminetto/clean-architecture-go-v2/usecase/book" "github.com/eminetto/clean-architecture-go-v2/api/presenter" - "github.com/eminetto/clean-architecture-go-v2/domain/entity" - "github.com/eminetto/clean-architecture-go-v2/domain/entity/book" - "github.com/codegangsta/negroni" + "github.com/eminetto/clean-architecture-go-v2/entity" "github.com/gorilla/mux" ) -func listBooks(manager book.Manager) http.Handler { +func listBooks(service book.UseCase) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { errorMessage := "Error reading books" - var data []*book.Book + var data []*entity.Book var err error title := r.URL.Query().Get("title") switch { case title == "": - data, err = manager.List() + data, err = service.ListBooks() default: - data, err = manager.Search(title) + data, err = service.SearchBooks(title) } w.Header().Set("Content-Type", "application/json") - if err != nil && err != domain.ErrNotFound { + if err != nil && err != entity.ErrNotFound { w.WriteHeader(http.StatusInternalServerError) w.Write([]byte(errorMessage)) return @@ -58,7 +55,7 @@ func listBooks(manager book.Manager) http.Handler { }) } -func createBook(manager book.Manager) http.Handler { +func createBook(service book.UseCase) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { errorMessage := "Error adding book" var input struct { @@ -74,27 +71,18 @@ func createBook(manager book.Manager) http.Handler { w.Write([]byte(errorMessage)) return } - b := &book.Book{ - ID: entity.NewID(), - Title: input.Title, - Author: input.Author, - Pages: input.Pages, - Quantity: input.Quantity, - CreatedAt: time.Now(), - } - b.ID, err = manager.Create(b) + id, err := service.CreateBook(input.Title, input.Author, input.Pages, input.Quantity) if err != nil { - log.Println(err.Error()) w.WriteHeader(http.StatusInternalServerError) w.Write([]byte(errorMessage)) return } toJ := &presenter.Book{ - ID: b.ID, - Title: b.Title, - Author: b.Author, - Pages: b.Pages, - Quantity: b.Quantity, + ID: id, + Title: input.Title, + Author: input.Author, + Pages: input.Pages, + Quantity: input.Quantity, } w.WriteHeader(http.StatusCreated) @@ -107,7 +95,7 @@ func createBook(manager book.Manager) http.Handler { }) } -func getBook(manager book.Manager) http.Handler { +func getBook(service book.UseCase) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { errorMessage := "Error reading book" vars := mux.Vars(r) @@ -117,8 +105,8 @@ func getBook(manager book.Manager) http.Handler { w.Write([]byte(errorMessage)) return } - data, err := manager.Get(id) - if err != nil && err != domain.ErrNotFound { + data, err := service.GetBook(id) + if err != nil && err != entity.ErrNotFound { w.WriteHeader(http.StatusInternalServerError) w.Write([]byte(errorMessage)) return @@ -143,7 +131,7 @@ func getBook(manager book.Manager) http.Handler { }) } -func deleteBook(manager book.Manager) http.Handler { +func deleteBook(service book.UseCase) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { errorMessage := "Error removing bookmark" vars := mux.Vars(r) @@ -153,7 +141,7 @@ func deleteBook(manager book.Manager) http.Handler { w.Write([]byte(errorMessage)) return } - err = manager.Delete(id) + err = service.DeleteBook(id) if err != nil { w.WriteHeader(http.StatusInternalServerError) w.Write([]byte(errorMessage)) @@ -163,20 +151,20 @@ func deleteBook(manager book.Manager) http.Handler { } //MakeBookHandlers make url handlers -func MakeBookHandlers(r *mux.Router, n negroni.Negroni, manager book.Manager) { +func MakeBookHandlers(r *mux.Router, n negroni.Negroni, service book.UseCase) { r.Handle("/v1/book", n.With( - negroni.Wrap(listBooks(manager)), + negroni.Wrap(listBooks(service)), )).Methods("GET", "OPTIONS").Name("listBooks") r.Handle("/v1/book", n.With( - negroni.Wrap(createBook(manager)), + negroni.Wrap(createBook(service)), )).Methods("POST", "OPTIONS").Name("createBook") r.Handle("/v1/book/{id}", n.With( - negroni.Wrap(getBook(manager)), + negroni.Wrap(getBook(service)), )).Methods("GET", "OPTIONS").Name("getBook") r.Handle("/v1/book/{id}", n.With( - negroni.Wrap(deleteBook(manager)), + negroni.Wrap(deleteBook(service)), )).Methods("DELETE", "OPTIONS").Name("deleteBook") } diff --git a/api/handler/book_test.go b/api/handler/book_test.go index 9794b4d..cf2c6b5 100644 --- a/api/handler/book_test.go +++ b/api/handler/book_test.go @@ -8,13 +8,10 @@ import ( "strings" "testing" - "github.com/eminetto/clean-architecture-go-v2/domain" - "github.com/eminetto/clean-architecture-go-v2/domain/entity" - - "github.com/eminetto/clean-architecture-go-v2/domain/entity/book" + "github.com/eminetto/clean-architecture-go-v2/entity" "github.com/codegangsta/negroni" - "github.com/eminetto/clean-architecture-go-v2/domain/entity/book/mock" + "github.com/eminetto/clean-architecture-go-v2/usecase/book/mock" "github.com/golang/mock/gomock" "github.com/gorilla/mux" "github.com/stretchr/testify/assert" @@ -23,18 +20,20 @@ import ( func Test_listBooks(t *testing.T) { controller := gomock.NewController(t) defer controller.Finish() - manager := mock.NewMockManager(controller) + service := mock.NewMockUseCase(controller) r := mux.NewRouter() n := negroni.New() - MakeBookHandlers(r, *n, manager) + MakeBookHandlers(r, *n, service) path, err := r.GetRoute("listBooks").GetPathTemplate() assert.Nil(t, err) assert.Equal(t, "/v1/book", path) - b := book.NewFixtureBook() - manager.EXPECT(). - List(). - Return([]*book.Book{b}, nil) - ts := httptest.NewServer(listBooks(manager)) + b := &entity.Book{ + ID: entity.NewID(), + } + service.EXPECT(). + ListBooks(). + Return([]*entity.Book{b}, nil) + ts := httptest.NewServer(listBooks(service)) defer ts.Close() res, err := http.Get(ts.URL) assert.Nil(t, err) @@ -44,12 +43,12 @@ func Test_listBooks(t *testing.T) { func Test_listBooks_NotFound(t *testing.T) { controller := gomock.NewController(t) defer controller.Finish() - manager := mock.NewMockManager(controller) - ts := httptest.NewServer(listBooks(manager)) + service := mock.NewMockUseCase(controller) + ts := httptest.NewServer(listBooks(service)) defer ts.Close() - manager.EXPECT(). - Search("book of books"). - Return(nil, domain.ErrNotFound) + service.EXPECT(). + SearchBooks("book of books"). + Return(nil, entity.ErrNotFound) res, err := http.Get(ts.URL + "?title=book+of+books") assert.Nil(t, err) assert.Equal(t, http.StatusNotFound, res.StatusCode) @@ -58,12 +57,14 @@ func Test_listBooks_NotFound(t *testing.T) { func Test_listBooks_Search(t *testing.T) { controller := gomock.NewController(t) defer controller.Finish() - manager := mock.NewMockManager(controller) - b := book.NewFixtureBook() - manager.EXPECT(). - Search("ozzy"). - Return([]*book.Book{b}, nil) - ts := httptest.NewServer(listBooks(manager)) + service := mock.NewMockUseCase(controller) + b := &entity.Book{ + ID: entity.NewID(), + } + service.EXPECT(). + SearchBooks("ozzy"). + Return([]*entity.Book{b}, nil) + ts := httptest.NewServer(listBooks(service)) defer ts.Close() res, err := http.Get(ts.URL + "?title=ozzy") assert.Nil(t, err) @@ -73,18 +74,18 @@ func Test_listBooks_Search(t *testing.T) { func Test_createBook(t *testing.T) { controller := gomock.NewController(t) defer controller.Finish() - manager := mock.NewMockManager(controller) + service := mock.NewMockUseCase(controller) r := mux.NewRouter() n := negroni.New() - MakeBookHandlers(r, *n, manager) + MakeBookHandlers(r, *n, service) path, err := r.GetRoute("createBook").GetPathTemplate() assert.Nil(t, err) assert.Equal(t, "/v1/book", path) - manager.EXPECT(). - Create(gomock.Any()). + service.EXPECT(). + CreateBook(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). Return(entity.NewID(), nil) - h := createBook(manager) + h := createBook(service) ts := httptest.NewServer(h) defer ts.Close() @@ -97,7 +98,7 @@ func Test_createBook(t *testing.T) { resp, _ := http.Post(ts.URL+"/v1/book", "application/json", strings.NewReader(payload)) assert.Equal(t, http.StatusCreated, resp.StatusCode) - var b *book.Book + var b *entity.Book json.NewDecoder(resp.Body).Decode(&b) assert.Equal(t, "Ozzy Osbourne", b.Author) } @@ -105,25 +106,27 @@ func Test_createBook(t *testing.T) { func Test_getBook(t *testing.T) { controller := gomock.NewController(t) defer controller.Finish() - manager := mock.NewMockManager(controller) + service := mock.NewMockUseCase(controller) r := mux.NewRouter() n := negroni.New() - MakeBookHandlers(r, *n, manager) + MakeBookHandlers(r, *n, service) path, err := r.GetRoute("getBook").GetPathTemplate() assert.Nil(t, err) assert.Equal(t, "/v1/book/{id}", path) - b := book.NewFixtureBook() - manager.EXPECT(). - Get(b.ID). + b := &entity.Book{ + ID: entity.NewID(), + } + service.EXPECT(). + GetBook(b.ID). Return(b, nil) - handler := getBook(manager) + handler := getBook(service) r.Handle("/v1/book/{id}", handler) ts := httptest.NewServer(r) defer ts.Close() res, err := http.Get(ts.URL + "/v1/book/" + b.ID.String()) assert.Nil(t, err) assert.Equal(t, http.StatusOK, res.StatusCode) - var d *book.Book + var d *entity.Book json.NewDecoder(res.Body).Decode(&d) assert.NotNil(t, d) assert.Equal(t, b.ID, d.ID) @@ -132,16 +135,18 @@ func Test_getBook(t *testing.T) { func Test_deleteBook(t *testing.T) { controller := gomock.NewController(t) defer controller.Finish() - manager := mock.NewMockManager(controller) + service := mock.NewMockUseCase(controller) r := mux.NewRouter() n := negroni.New() - MakeBookHandlers(r, *n, manager) + MakeBookHandlers(r, *n, service) path, err := r.GetRoute("deleteBook").GetPathTemplate() assert.Nil(t, err) assert.Equal(t, "/v1/book/{id}", path) - b := book.NewFixtureBook() - manager.EXPECT().Delete(b.ID).Return(nil) - handler := deleteBook(manager) + b := &entity.Book{ + ID: entity.NewID(), + } + service.EXPECT().DeleteBook(b.ID).Return(nil) + handler := deleteBook(service) req, _ := http.NewRequest("DELETE", "/v1/bookmark/"+b.ID.String(), nil) r.Handle("/v1/bookmark/{id}", handler).Methods("DELETE", "OPTIONS") rr := httptest.NewRecorder() diff --git a/api/handler/loan.go b/api/handler/loan.go index 639f7d0..16ea7e0 100644 --- a/api/handler/loan.go +++ b/api/handler/loan.go @@ -4,20 +4,17 @@ import ( "fmt" "net/http" - "github.com/eminetto/clean-architecture-go-v2/domain/entity/user" + "github.com/eminetto/clean-architecture-go-v2/usecase/book" + "github.com/eminetto/clean-architecture-go-v2/usecase/user" - "github.com/eminetto/clean-architecture-go-v2/domain/usecase/loan" - - "github.com/eminetto/clean-architecture-go-v2/domain" - - "github.com/eminetto/clean-architecture-go-v2/domain/entity" - "github.com/eminetto/clean-architecture-go-v2/domain/entity/book" + "github.com/eminetto/clean-architecture-go-v2/usecase/loan" "github.com/codegangsta/negroni" + "github.com/eminetto/clean-architecture-go-v2/entity" "github.com/gorilla/mux" ) -func borrowBook(bManager book.Manager, uManager user.Manager, loanUseCase loan.UseCase) http.Handler { +func borrowBook(bookService book.UseCase, userService user.UseCase, loanService loan.UseCase) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { errorMessage := "Error borrowing book" vars := mux.Vars(r) @@ -27,8 +24,8 @@ func borrowBook(bManager book.Manager, uManager user.Manager, loanUseCase loan.U w.Write([]byte(errorMessage)) return } - b, err := bManager.Get(bID) - if err != nil && err != domain.ErrNotFound { + b, err := bookService.GetBook(bID) + if err != nil && err != entity.ErrNotFound { w.WriteHeader(http.StatusInternalServerError) w.Write([]byte(errorMessage)) return @@ -44,8 +41,8 @@ func borrowBook(bManager book.Manager, uManager user.Manager, loanUseCase loan.U w.Write([]byte(errorMessage)) return } - u, err := uManager.Get(uID) - if err != nil && err != domain.ErrNotFound { + u, err := userService.GetUser(uID) + if err != nil && err != entity.ErrNotFound { w.WriteHeader(http.StatusInternalServerError) w.Write([]byte(errorMessage)) return @@ -55,7 +52,7 @@ func borrowBook(bManager book.Manager, uManager user.Manager, loanUseCase loan.U w.Write([]byte(errorMessage)) return } - err = loanUseCase.Borrow(u, b) + err = loanService.Borrow(u, b) if err != nil { fmt.Println(err) w.WriteHeader(http.StatusInternalServerError) @@ -66,7 +63,7 @@ func borrowBook(bManager book.Manager, uManager user.Manager, loanUseCase loan.U }) } -func returnBook(bManager book.Manager, loanUseCase loan.UseCase) http.Handler { +func returnBook(bookService book.UseCase, loanService loan.UseCase) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { errorMessage := "Error returning book" vars := mux.Vars(r) @@ -76,8 +73,8 @@ func returnBook(bManager book.Manager, loanUseCase loan.UseCase) http.Handler { w.Write([]byte(errorMessage)) return } - b, err := bManager.Get(bID) - if err != nil && err != domain.ErrNotFound { + b, err := bookService.GetBook(bID) + if err != nil && err != entity.ErrNotFound { w.WriteHeader(http.StatusInternalServerError) w.Write([]byte(errorMessage)) return @@ -87,8 +84,8 @@ func returnBook(bManager book.Manager, loanUseCase loan.UseCase) http.Handler { w.Write([]byte(errorMessage)) return } - err = loanUseCase.Return(b) - if err != nil && err != domain.ErrNotFound { + err = loanService.Return(b) + if err != nil && err != entity.ErrNotFound { w.WriteHeader(http.StatusInternalServerError) w.Write([]byte(errorMessage)) return @@ -98,12 +95,12 @@ func returnBook(bManager book.Manager, loanUseCase loan.UseCase) http.Handler { } //MakeLoanHandlers make url handlers -func MakeLoanHandlers(r *mux.Router, n negroni.Negroni, bManager book.Manager, uManager user.Manager, loanUseCase loan.UseCase) { +func MakeLoanHandlers(r *mux.Router, n negroni.Negroni, bookService book.UseCase, userService user.UseCase, loanService loan.UseCase) { r.Handle("/v1/loan/borrow/{book_id}/{user_id}", n.With( - negroni.Wrap(borrowBook(bManager, uManager, loanUseCase)), + negroni.Wrap(borrowBook(bookService, userService, loanService)), )).Methods("GET", "OPTIONS").Name("borrowBook") r.Handle("/v1/loan/return/{book_id}", n.With( - negroni.Wrap(returnBook(bManager, loanUseCase)), + negroni.Wrap(returnBook(bookService, loanService)), )).Methods("GET", "OPTIONS").Name("returnBook") } diff --git a/api/handler/loan_test.go b/api/handler/loan_test.go index 5f28b61..7131c9c 100644 --- a/api/handler/loan_test.go +++ b/api/handler/loan_test.go @@ -6,17 +6,12 @@ import ( "net/http/httptest" "testing" - "github.com/eminetto/clean-architecture-go-v2/domain/entity/user" - - "github.com/eminetto/clean-architecture-go-v2/domain/entity/book" - - "github.com/eminetto/clean-architecture-go-v2/domain" - "github.com/eminetto/clean-architecture-go-v2/domain/entity" + "github.com/eminetto/clean-architecture-go-v2/entity" "github.com/codegangsta/negroni" - bmock "github.com/eminetto/clean-architecture-go-v2/domain/entity/book/mock" - umock "github.com/eminetto/clean-architecture-go-v2/domain/entity/user/mock" - lmock "github.com/eminetto/clean-architecture-go-v2/domain/usecase/loan/mock" + bmock "github.com/eminetto/clean-architecture-go-v2/usecase/book/mock" + lmock "github.com/eminetto/clean-architecture-go-v2/usecase/loan/mock" + umock "github.com/eminetto/clean-architecture-go-v2/usecase/user/mock" "github.com/golang/mock/gomock" "github.com/gorilla/mux" "github.com/stretchr/testify/assert" @@ -25,8 +20,8 @@ import ( func Test_borrowBook(t *testing.T) { controller := gomock.NewController(t) defer controller.Finish() - uMock := umock.NewMockManager(controller) - bMock := bmock.NewMockManager(controller) + uMock := umock.NewMockUseCase(controller) + bMock := bmock.NewMockUseCase(controller) lMock := lmock.NewMockUseCase(controller) r := mux.NewRouter() n := negroni.New() @@ -39,7 +34,7 @@ func Test_borrowBook(t *testing.T) { t.Run("book not found", func(t *testing.T) { bID := entity.NewID() uID := entity.NewID() - bMock.EXPECT().Get(bID).Return(nil, domain.ErrNotFound) + bMock.EXPECT().GetBook(bID).Return(nil, entity.ErrNotFound) ts := httptest.NewServer(r) defer ts.Close() res, err := http.Get(fmt.Sprintf("%s/v1/loan/borrow/%s/%s", ts.URL, bID.String(), uID.String())) @@ -47,10 +42,12 @@ func Test_borrowBook(t *testing.T) { assert.Equal(t, http.StatusNotFound, res.StatusCode) }) t.Run("user not found", func(t *testing.T) { - b := book.NewFixtureBook() + b := &entity.Book{ + ID: entity.NewID(), + } uID := entity.NewID() - bMock.EXPECT().Get(b.ID).Return(b, nil) - uMock.EXPECT().Get(uID).Return(nil, domain.ErrNotFound) + bMock.EXPECT().GetBook(b.ID).Return(b, nil) + uMock.EXPECT().GetUser(uID).Return(nil, entity.ErrNotFound) ts := httptest.NewServer(r) defer ts.Close() res, err := http.Get(fmt.Sprintf("%s/v1/loan/borrow/%s/%s", ts.URL, b.ID.String(), uID.String())) @@ -58,10 +55,14 @@ func Test_borrowBook(t *testing.T) { assert.Equal(t, http.StatusNotFound, res.StatusCode) }) t.Run("success", func(t *testing.T) { - b := book.NewFixtureBook() - u := user.NewFixtureUser() - bMock.EXPECT().Get(b.ID).Return(b, nil) - uMock.EXPECT().Get(u.ID).Return(u, nil) + b := &entity.Book{ + ID: entity.NewID(), + } + u := &entity.User{ + ID: entity.NewID(), + } + bMock.EXPECT().GetBook(b.ID).Return(b, nil) + uMock.EXPECT().GetUser(u.ID).Return(u, nil) lMock.EXPECT().Borrow(u, b).Return(nil) ts := httptest.NewServer(r) defer ts.Close() @@ -74,8 +75,8 @@ func Test_borrowBook(t *testing.T) { func Test_returnBook(t *testing.T) { controller := gomock.NewController(t) defer controller.Finish() - uMock := umock.NewMockManager(controller) - bMock := bmock.NewMockManager(controller) + uMock := umock.NewMockUseCase(controller) + bMock := bmock.NewMockUseCase(controller) lMock := lmock.NewMockUseCase(controller) r := mux.NewRouter() n := negroni.New() @@ -87,7 +88,7 @@ func Test_returnBook(t *testing.T) { r.Handle("/v1/loan/return/{book_id}", handler) t.Run("book not found", func(t *testing.T) { bID := entity.NewID() - bMock.EXPECT().Get(bID).Return(nil, domain.ErrNotFound) + bMock.EXPECT().GetBook(bID).Return(nil, entity.ErrNotFound) ts := httptest.NewServer(r) defer ts.Close() res, err := http.Get(fmt.Sprintf("%s/v1/loan/return/%s", ts.URL, bID.String())) @@ -95,8 +96,10 @@ func Test_returnBook(t *testing.T) { assert.Equal(t, http.StatusNotFound, res.StatusCode) }) t.Run("success", func(t *testing.T) { - b := book.NewFixtureBook() - bMock.EXPECT().Get(b.ID).Return(b, nil) + b := &entity.Book{ + ID: entity.NewID(), + } + bMock.EXPECT().GetBook(b.ID).Return(b, nil) lMock.EXPECT().Return(b).Return(nil) ts := httptest.NewServer(r) defer ts.Close() diff --git a/api/handler/user.go b/api/handler/user.go index 6c7f2a0..4b1b45f 100644 --- a/api/handler/user.go +++ b/api/handler/user.go @@ -4,34 +4,31 @@ import ( "encoding/json" "log" "net/http" - "time" - "github.com/eminetto/clean-architecture-go-v2/domain/entity/user" - - "github.com/eminetto/clean-architecture-go-v2/domain" + "github.com/eminetto/clean-architecture-go-v2/usecase/user" "github.com/eminetto/clean-architecture-go-v2/api/presenter" - "github.com/eminetto/clean-architecture-go-v2/domain/entity" + "github.com/eminetto/clean-architecture-go-v2/entity" "github.com/codegangsta/negroni" "github.com/gorilla/mux" ) -func listUsers(manager user.Manager) http.Handler { +func listUsers(service user.UseCase) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { errorMessage := "Error reading users" - var data []*user.User + var data []*entity.User var err error name := r.URL.Query().Get("name") switch { case name == "": - data, err = manager.List() + data, err = service.ListUsers() default: - data, err = manager.Search(name) + data, err = service.SearchUsers(name) } w.Header().Set("Content-Type", "application/json") - if err != nil && err != domain.ErrNotFound { + if err != nil && err != entity.ErrNotFound { w.WriteHeader(http.StatusInternalServerError) w.Write([]byte(errorMessage)) return @@ -58,7 +55,7 @@ func listUsers(manager user.Manager) http.Handler { }) } -func createUser(manager user.Manager) http.Handler { +func createUser(service user.UseCase) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { errorMessage := "Error adding user" var input struct { @@ -74,16 +71,7 @@ func createUser(manager user.Manager) http.Handler { w.Write([]byte(errorMessage)) return } - //TODO: validate data ;) - u := &user.User{ - ID: entity.NewID(), - Email: input.Email, - Password: input.Password, - FirstName: input.FirstName, - LastName: input.LastName, - CreatedAt: time.Now(), - } - u.ID, err = manager.Create(u) + id, err := service.CreateUser(input.Email, input.Password, input.FirstName, input.LastName) if err != nil { log.Println(err.Error()) w.WriteHeader(http.StatusInternalServerError) @@ -91,15 +79,14 @@ func createUser(manager user.Manager) http.Handler { return } toJ := &presenter.User{ - ID: u.ID, - Email: u.Email, - FirstName: u.FirstName, - LastName: u.LastName, + ID: id, + Email: input.Email, + FirstName: input.FirstName, + LastName: input.LastName, } w.WriteHeader(http.StatusCreated) if err := json.NewEncoder(w).Encode(toJ); err != nil { - log.Println(err.Error()) w.WriteHeader(http.StatusInternalServerError) w.Write([]byte(errorMessage)) return @@ -107,7 +94,7 @@ func createUser(manager user.Manager) http.Handler { }) } -func getUser(manager user.Manager) http.Handler { +func getUser(service user.UseCase) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { errorMessage := "Error reading user" vars := mux.Vars(r) @@ -117,9 +104,9 @@ func getUser(manager user.Manager) http.Handler { w.Write([]byte(errorMessage)) return } - data, err := manager.Get(id) + data, err := service.GetUser(id) w.Header().Set("Content-Type", "application/json") - if err != nil && err != domain.ErrNotFound { + if err != nil && err != entity.ErrNotFound { w.WriteHeader(http.StatusInternalServerError) w.Write([]byte(errorMessage)) return @@ -143,7 +130,7 @@ func getUser(manager user.Manager) http.Handler { }) } -func deleteUser(manager user.Manager) http.Handler { +func deleteUser(service user.UseCase) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { errorMessage := "Error removing user" vars := mux.Vars(r) @@ -153,7 +140,7 @@ func deleteUser(manager user.Manager) http.Handler { w.Write([]byte(errorMessage)) return } - err = manager.Delete(id) + err = service.DeleteUser(id) if err != nil { w.WriteHeader(http.StatusInternalServerError) w.Write([]byte(errorMessage)) @@ -163,20 +150,20 @@ func deleteUser(manager user.Manager) http.Handler { } //MakeUserHandlers make url handlers -func MakeUserHandlers(r *mux.Router, n negroni.Negroni, manager user.Manager) { +func MakeUserHandlers(r *mux.Router, n negroni.Negroni, service user.UseCase) { r.Handle("/v1/user", n.With( - negroni.Wrap(listUsers(manager)), + negroni.Wrap(listUsers(service)), )).Methods("GET", "OPTIONS").Name("listUsers") r.Handle("/v1/user", n.With( - negroni.Wrap(createUser(manager)), + negroni.Wrap(createUser(service)), )).Methods("POST", "OPTIONS").Name("createUser") r.Handle("/v1/user/{id}", n.With( - negroni.Wrap(getUser(manager)), + negroni.Wrap(getUser(service)), )).Methods("GET", "OPTIONS").Name("getUser") r.Handle("/v1/user/{id}", n.With( - negroni.Wrap(deleteUser(manager)), + negroni.Wrap(deleteUser(service)), )).Methods("DELETE", "OPTIONS").Name("deleteUser") } diff --git a/api/handler/user_test.go b/api/handler/user_test.go index 64dcd54..05b06de 100644 --- a/api/handler/user_test.go +++ b/api/handler/user_test.go @@ -10,10 +10,8 @@ import ( "github.com/codegangsta/negroni" "github.com/eminetto/clean-architecture-go-v2/api/presenter" - "github.com/eminetto/clean-architecture-go-v2/domain" - "github.com/eminetto/clean-architecture-go-v2/domain/entity" - "github.com/eminetto/clean-architecture-go-v2/domain/entity/user" - "github.com/eminetto/clean-architecture-go-v2/domain/entity/user/mock" + "github.com/eminetto/clean-architecture-go-v2/entity" + "github.com/eminetto/clean-architecture-go-v2/usecase/user/mock" "github.com/golang/mock/gomock" "github.com/gorilla/mux" "github.com/stretchr/testify/assert" @@ -22,17 +20,19 @@ import ( func Test_listUsers(t *testing.T) { controller := gomock.NewController(t) defer controller.Finish() - m := mock.NewMockManager(controller) + m := mock.NewMockUseCase(controller) r := mux.NewRouter() n := negroni.New() MakeUserHandlers(r, *n, m) path, err := r.GetRoute("listUsers").GetPathTemplate() assert.Nil(t, err) assert.Equal(t, "/v1/user", path) - u := user.NewFixtureUser() + u := &entity.User{ + ID: entity.NewID(), + } m.EXPECT(). - List(). - Return([]*user.User{u}, nil) + ListUsers(). + Return([]*entity.User{u}, nil) ts := httptest.NewServer(listUsers(m)) defer ts.Close() res, err := http.Get(ts.URL) @@ -43,12 +43,12 @@ func Test_listUsers(t *testing.T) { func Test_listUsers_NotFound(t *testing.T) { controller := gomock.NewController(t) defer controller.Finish() - m := mock.NewMockManager(controller) + m := mock.NewMockUseCase(controller) ts := httptest.NewServer(listUsers(m)) defer ts.Close() m.EXPECT(). - Search("dio"). - Return(nil, domain.ErrNotFound) + SearchUsers("dio"). + Return(nil, entity.ErrNotFound) res, err := http.Get(ts.URL + "?name=dio") assert.Nil(t, err) assert.Equal(t, http.StatusNotFound, res.StatusCode) @@ -57,11 +57,13 @@ func Test_listUsers_NotFound(t *testing.T) { func Test_listUsers_Search(t *testing.T) { controller := gomock.NewController(t) defer controller.Finish() - m := mock.NewMockManager(controller) - u := user.NewFixtureUser() + m := mock.NewMockUseCase(controller) + u := &entity.User{ + ID: entity.NewID(), + } m.EXPECT(). - Search("ozzy"). - Return([]*user.User{u}, nil) + SearchUsers("ozzy"). + Return([]*entity.User{u}, nil) ts := httptest.NewServer(listUsers(m)) defer ts.Close() res, err := http.Get(ts.URL + "?name=ozzy") @@ -72,7 +74,7 @@ func Test_listUsers_Search(t *testing.T) { func Test_createUser(t *testing.T) { controller := gomock.NewController(t) defer controller.Finish() - m := mock.NewMockManager(controller) + m := mock.NewMockUseCase(controller) r := mux.NewRouter() n := negroni.New() MakeUserHandlers(r, *n, m) @@ -81,7 +83,7 @@ func Test_createUser(t *testing.T) { assert.Equal(t, "/v1/user", path) m.EXPECT(). - Create(gomock.Any()). + CreateUser(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). Return(entity.NewID(), nil) h := createUser(m) @@ -105,16 +107,18 @@ func Test_createUser(t *testing.T) { func Test_getUser(t *testing.T) { controller := gomock.NewController(t) defer controller.Finish() - m := mock.NewMockManager(controller) + m := mock.NewMockUseCase(controller) r := mux.NewRouter() n := negroni.New() MakeUserHandlers(r, *n, m) path, err := r.GetRoute("getUser").GetPathTemplate() assert.Nil(t, err) assert.Equal(t, "/v1/user/{id}", path) - u := user.NewFixtureUser() + u := &entity.User{ + ID: entity.NewID(), + } m.EXPECT(). - Get(u.ID). + GetUser(u.ID). Return(u, nil) handler := getUser(m) r.Handle("/v1/user/{id}", handler) @@ -132,15 +136,17 @@ func Test_getUser(t *testing.T) { func Test_deleteUser(t *testing.T) { controller := gomock.NewController(t) defer controller.Finish() - m := mock.NewMockManager(controller) + m := mock.NewMockUseCase(controller) r := mux.NewRouter() n := negroni.New() MakeUserHandlers(r, *n, m) path, err := r.GetRoute("deleteUser").GetPathTemplate() assert.Nil(t, err) assert.Equal(t, "/v1/user/{id}", path) - u := user.NewFixtureUser() - m.EXPECT().Delete(u.ID).Return(nil) + u := &entity.User{ + ID: entity.NewID(), + } + m.EXPECT().DeleteUser(u.ID).Return(nil) handler := deleteUser(m) req, _ := http.NewRequest("DELETE", "/v1/user/"+u.ID.String(), nil) r.Handle("/v1/user/{id}", handler).Methods("DELETE", "OPTIONS") diff --git a/api/main.go b/api/main.go index d040ef1..20ab983 100644 --- a/api/main.go +++ b/api/main.go @@ -9,13 +9,12 @@ import ( "strconv" "time" - "github.com/eminetto/clean-architecture-go-v2/pkg/password" + "github.com/eminetto/clean-architecture-go-v2/driver/repository" + "github.com/eminetto/clean-architecture-go-v2/usecase/book" + "github.com/eminetto/clean-architecture-go-v2/usecase/user" - "github.com/eminetto/clean-architecture-go-v2/domain/usecase/loan" + "github.com/eminetto/clean-architecture-go-v2/usecase/loan" - "github.com/eminetto/clean-architecture-go-v2/domain/entity/user" - - "github.com/eminetto/clean-architecture-go-v2/domain/entity/book" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/codegangsta/negroni" @@ -37,13 +36,13 @@ func main() { } defer db.Close() - bookRepo := book.NewMySQLRepository(db) - bookManager := book.NewManager(bookRepo) + bookRepo := repository.NewBookMySQL(db) + bookService := book.NewService(bookRepo) - userRepo := user.NewMySQLRepoRepository(db) - userManager := user.NewManager(userRepo, password.NewService()) + userRepo := repository.NewUserMySQL(db) + userService := user.NewService(userRepo) - loanUseCase := loan.NewUseCase(userManager, bookManager) + loanUseCase := loan.NewService(userService, bookService) metricService, err := metric.NewPrometheusService() if err != nil { @@ -57,13 +56,13 @@ func main() { negroni.NewLogger(), ) //book - handler.MakeBookHandlers(r, *n, bookManager) + handler.MakeBookHandlers(r, *n, bookService) //user - handler.MakeUserHandlers(r, *n, userManager) + handler.MakeUserHandlers(r, *n, userService) //loan - handler.MakeLoanHandlers(r, *n, bookManager, userManager, loanUseCase) + handler.MakeLoanHandlers(r, *n, bookService, userService, loanUseCase) http.Handle("/", r) http.Handle("/metrics", promhttp.Handler()) diff --git a/api/presenter/book.go b/api/presenter/book.go index 257c7e5..ba52eb3 100644 --- a/api/presenter/book.go +++ b/api/presenter/book.go @@ -1,7 +1,7 @@ package presenter import ( - "github.com/eminetto/clean-architecture-go-v2/domain/entity" + "github.com/eminetto/clean-architecture-go-v2/entity" ) //Book data diff --git a/api/presenter/user.go b/api/presenter/user.go index 05dfb3e..bf4b97e 100644 --- a/api/presenter/user.go +++ b/api/presenter/user.go @@ -1,7 +1,7 @@ package presenter import ( - "github.com/eminetto/clean-architecture-go-v2/domain/entity" + "github.com/eminetto/clean-architecture-go-v2/entity" ) //User data diff --git a/cmd/main.go b/cmd/main.go index ff2696d..f1041d7 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -7,11 +7,12 @@ import ( "log" "os" + "github.com/eminetto/clean-architecture-go-v2/driver/repository" + "github.com/eminetto/clean-architecture-go-v2/usecase/book" + "github.com/eminetto/clean-architecture-go-v2/config" _ "github.com/go-sql-driver/mysql" - "github.com/eminetto/clean-architecture-go-v2/domain/entity/book" - "github.com/eminetto/clean-architecture-go-v2/pkg/metric" ) @@ -40,9 +41,9 @@ func main() { log.Fatal(err.Error()) } defer db.Close() - repo := book.NewMySQLRepository(db) - manager := book.NewManager(repo) - all, err := manager.Search(query) + repo := repository.NewBookMySQL(db) + service := book.NewService(repo) + all, err := service.SearchBooks(query) if err != nil { log.Fatal(err) } diff --git a/docker-compose.yml b/docker-compose.yml index 6894b3b..82d0aab 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,7 +4,7 @@ services: image: mysql:5.7 command: --default-authentication-plugin=mysql_native_password --init-file /data/application/init.sql volumes: - - ./infra/init.sql:/data/application/init.sql + - ./ops/init.sql:/data/application/init.sql environment: MYSQL_ROOT_PASSWORD: clean_architecture_go_v2 MYSQL_DATABASE: clean_architecture_go_v2 @@ -30,7 +30,7 @@ services: command: - --config.file=/etc/prometheus/prometheus.yml volumes: - - ./infra/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro + - ./ops/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro container_name: clean-architecture-go-v2-prometheus network_mode: "bridge" prometheus-pushgateway: diff --git a/domain/entity/book/entity.go b/domain/entity/book/entity.go deleted file mode 100644 index cd6a575..0000000 --- a/domain/entity/book/entity.go +++ /dev/null @@ -1,18 +0,0 @@ -package book - -import ( - "time" - - "github.com/eminetto/clean-architecture-go-v2/domain/entity" -) - -//Book data -type Book struct { - ID entity.ID - Title string - Author string - Pages int - Quantity int - CreatedAt time.Time - UpdatedAt time.Time -} diff --git a/domain/entity/book/fixture.go b/domain/entity/book/fixture.go deleted file mode 100644 index 97a3bca..0000000 --- a/domain/entity/book/fixture.go +++ /dev/null @@ -1,18 +0,0 @@ -package book - -import ( - "time" - - "github.com/eminetto/clean-architecture-go-v2/domain/entity" -) - -func NewFixtureBook() *Book { - return &Book{ - ID: entity.NewID(), - Title: "I Am Ozzy", - Author: "Ozzy Osbourne", - Pages: 294, - Quantity: 1, - CreatedAt: time.Now(), - } -} diff --git a/domain/entity/book/interface.go b/domain/entity/book/interface.go deleted file mode 100644 index 233e6cb..0000000 --- a/domain/entity/book/interface.go +++ /dev/null @@ -1,28 +0,0 @@ -package book - -import "github.com/eminetto/clean-architecture-go-v2/domain/entity" - -//Reader interface -type Reader interface { - Get(id entity.ID) (*Book, error) - Search(query string) ([]*Book, error) - List() ([]*Book, error) -} - -//Writer book writer -type Writer interface { - Create(e *Book) (entity.ID, error) - Update(e *Book) error - Delete(id entity.ID) error -} - -//repository interface -type repository interface { - Reader - Writer -} - -//Manager interface -type Manager interface { - repository -} diff --git a/domain/entity/book/manager.go b/domain/entity/book/manager.go deleted file mode 100644 index 893dc2b..0000000 --- a/domain/entity/book/manager.go +++ /dev/null @@ -1,55 +0,0 @@ -package book - -import ( - "strings" - "time" - - "github.com/eminetto/clean-architecture-go-v2/domain/entity" -) - -type manager struct { - repo repository -} - -//NewManager create new manager -func NewManager(r repository) *manager { - return &manager{ - repo: r, - } -} - -//Create a book -func (s *manager) Create(e *Book) (entity.ID, error) { - e.ID = entity.NewID() - e.CreatedAt = time.Now() - return s.repo.Create(e) -} - -//Get a book -func (s *manager) Get(id entity.ID) (*Book, error) { - return s.repo.Get(id) -} - -//Search books -func (s *manager) Search(query string) ([]*Book, error) { - return s.repo.Search(strings.ToLower(query)) -} - -//List books -func (s *manager) List() ([]*Book, error) { - return s.repo.List() -} - -//Delete a book -func (s *manager) Delete(id entity.ID) error { - _, err := s.Get(id) - if err != nil { - return err - } - return s.repo.Delete(id) -} - -//Update a book -func (s *manager) Update(e *Book) error { - return s.repo.Update(e) -} diff --git a/domain/entity/book/manager_test.go b/domain/entity/book/manager_test.go deleted file mode 100644 index 2249154..0000000 --- a/domain/entity/book/manager_test.go +++ /dev/null @@ -1,82 +0,0 @@ -package book - -import ( - "testing" - - "github.com/eminetto/clean-architecture-go-v2/domain" - - "github.com/stretchr/testify/assert" -) - -func Test_Create(t *testing.T) { - repo := NewInmemRepository() - m := NewManager(repo) - u := NewFixtureBook() - id, err := m.Create(u) - assert.Nil(t, err) - assert.Equal(t, u.ID, id) - assert.False(t, u.CreatedAt.IsZero()) -} - -func Test_SearchAndFind(t *testing.T) { - repo := NewInmemRepository() - m := NewManager(repo) - u1 := NewFixtureBook() - u2 := NewFixtureBook() - u2.Title = "Lemmy: Biography" - - uID, _ := m.Create(u1) - _, _ = m.Create(u2) - - t.Run("search", func(t *testing.T) { - c, err := m.Search("ozzy") - assert.Nil(t, err) - assert.Equal(t, 1, len(c)) - assert.Equal(t, "I Am Ozzy", c[0].Title) - - c, err = m.Search("dio") - assert.Equal(t, domain.ErrNotFound, err) - assert.Nil(t, c) - }) - t.Run("list all", func(t *testing.T) { - all, err := m.List() - assert.Nil(t, err) - assert.Equal(t, 2, len(all)) - }) - - t.Run("get", func(t *testing.T) { - saved, err := m.Get(uID) - assert.Nil(t, err) - assert.Equal(t, u1.Title, saved.Title) - }) -} - -func Test_Update(t *testing.T) { - repo := NewInmemRepository() - m := NewManager(repo) - u := NewFixtureBook() - id, err := m.Create(u) - assert.Nil(t, err) - saved, _ := m.Get(id) - saved.Title = "Lemmy: Biography" - assert.Nil(t, m.Update(saved)) - updated, err := m.Get(id) - assert.Nil(t, err) - assert.Equal(t, "Lemmy: Biography", updated.Title) -} - -func TestDelete(t *testing.T) { - repo := NewInmemRepository() - m := NewManager(repo) - u1 := NewFixtureBook() - u2 := NewFixtureBook() - u2ID, _ := m.Create(u2) - - err := m.Delete(u1.ID) - assert.Equal(t, domain.ErrNotFound, err) - - err = m.Delete(u2ID) - assert.Nil(t, err) - _, err = m.Get(u2ID) - assert.Equal(t, domain.ErrNotFound, err) -} diff --git a/domain/entity/book/repository_inmem.go b/domain/entity/book/repository_inmem.go deleted file mode 100644 index 1410bea..0000000 --- a/domain/entity/book/repository_inmem.go +++ /dev/null @@ -1,78 +0,0 @@ -package book - -import ( - "strings" - - "github.com/eminetto/clean-architecture-go-v2/domain" - "github.com/eminetto/clean-architecture-go-v2/domain/entity" -) - -//iRepo in memory repo -type iRepo struct { - m map[entity.ID]*Book -} - -//NewInmemRepository create new repository -func NewInmemRepository() *iRepo { - var m = map[entity.ID]*Book{} - return &iRepo{ - m: m, - } -} - -//Create a book -func (r *iRepo) Create(e *Book) (entity.ID, error) { - r.m[e.ID] = e - return e.ID, nil -} - -//Get a book -func (r *iRepo) Get(id entity.ID) (*Book, error) { - if r.m[id] == nil { - return nil, domain.ErrNotFound - } - return r.m[id], nil -} - -//Update a book -func (r *iRepo) Update(e *Book) error { - _, err := r.Get(e.ID) - if err != nil { - return err - } - r.m[e.ID] = e - return nil -} - -//Search books -func (r *iRepo) Search(query string) ([]*Book, error) { - var d []*Book - for _, j := range r.m { - if strings.Contains(strings.ToLower(j.Title), query) { - d = append(d, j) - } - } - if len(d) == 0 { - return nil, domain.ErrNotFound - } - - return d, nil -} - -//List books -func (r *iRepo) List() ([]*Book, error) { - var d []*Book - for _, j := range r.m { - d = append(d, j) - } - return d, nil -} - -//Delete a book -func (r *iRepo) Delete(id entity.ID) error { - if r.m[id] == nil { - return domain.ErrNotFound - } - r.m[id] = nil - return nil -} diff --git a/domain/entity/user/entity.go b/domain/entity/user/entity.go deleted file mode 100644 index 5183b01..0000000 --- a/domain/entity/user/entity.go +++ /dev/null @@ -1,19 +0,0 @@ -package user - -import ( - "time" - - "github.com/eminetto/clean-architecture-go-v2/domain/entity" -) - -//User data -type User struct { - ID entity.ID - Email string - Password string - FirstName string - LastName string - CreatedAt time.Time - UpdatedAt time.Time - Books []entity.ID -} diff --git a/domain/entity/user/fixture.go b/domain/entity/user/fixture.go deleted file mode 100644 index 2b776c5..0000000 --- a/domain/entity/user/fixture.go +++ /dev/null @@ -1,18 +0,0 @@ -package user - -import ( - "time" - - "github.com/eminetto/clean-architecture-go-v2/domain/entity" -) - -func NewFixtureUser() *User { - return &User{ - ID: entity.NewID(), - Email: "ozzy@metalgods.net", - Password: "123456", - FirstName: "Ozzy", - LastName: "Osbourne", - CreatedAt: time.Now(), - } -} diff --git a/domain/entity/user/interface.go b/domain/entity/user/interface.go deleted file mode 100644 index edecb79..0000000 --- a/domain/entity/user/interface.go +++ /dev/null @@ -1,28 +0,0 @@ -package user - -import "github.com/eminetto/clean-architecture-go-v2/domain/entity" - -//Reader interface -type Reader interface { - Get(id entity.ID) (*User, error) - Search(query string) ([]*User, error) - List() ([]*User, error) -} - -//Writer user writer -type Writer interface { - Create(e *User) (entity.ID, error) - Update(e *User) error - Delete(id entity.ID) error -} - -//repository interface -type repository interface { - Reader - Writer -} - -//Manager interface -type Manager interface { - repository -} diff --git a/domain/entity/user/manager.go b/domain/entity/user/manager.go deleted file mode 100644 index 117c16e..0000000 --- a/domain/entity/user/manager.go +++ /dev/null @@ -1,71 +0,0 @@ -package user - -import ( - "strings" - "time" - - "github.com/eminetto/clean-architecture-go-v2/domain" - - "github.com/eminetto/clean-architecture-go-v2/pkg/password" - - "github.com/eminetto/clean-architecture-go-v2/domain/entity" -) - -//manager interface -type manager struct { - repo repository - pwd password.Service -} - -//NewManager create new repository -func NewManager(r repository, pwd password.Service) *manager { - return &manager{ - repo: r, - pwd: pwd, - } -} - -//Create an user -func (s *manager) Create(e *User) (entity.ID, error) { - e.ID = entity.NewID() - e.CreatedAt = time.Now() - pwd, err := s.pwd.Generate(e.Password) - if err != nil { - return e.ID, err - } - e.Password = pwd - return s.repo.Create(e) -} - -//Get an user -func (s *manager) Get(id entity.ID) (*User, error) { - return s.repo.Get(id) -} - -//Search users -func (s *manager) Search(query string) ([]*User, error) { - return s.repo.Search(strings.ToLower(query)) -} - -//List users -func (s *manager) List() ([]*User, error) { - return s.repo.List() -} - -//Delete an user -func (s *manager) Delete(id entity.ID) error { - u, err := s.Get(id) - if err != nil { - return err - } - if len(u.Books) > 0 { - return domain.ErrCannotBeDeleted - } - return s.repo.Delete(id) -} - -//Update an user -func (s *manager) Update(e *User) error { - e.UpdatedAt = time.Now() - return s.repo.Update(e) -} diff --git a/domain/entity/user/manager_test.go b/domain/entity/user/manager_test.go deleted file mode 100644 index dba9b6e..0000000 --- a/domain/entity/user/manager_test.go +++ /dev/null @@ -1,95 +0,0 @@ -package user - -import ( - "testing" - - "github.com/eminetto/clean-architecture-go-v2/pkg/password" - - "github.com/eminetto/clean-architecture-go-v2/domain" - "github.com/eminetto/clean-architecture-go-v2/domain/entity" - - "github.com/stretchr/testify/assert" -) - -func Test_Create(t *testing.T) { - repo := NewInmemRepository() - m := NewManager(repo, password.NewFakeService()) - u := NewFixtureUser() - id, err := m.Create(u) - assert.Nil(t, err) - assert.Equal(t, u.ID, id) - assert.False(t, u.CreatedAt.IsZero()) - assert.True(t, u.UpdatedAt.IsZero()) -} - -func Test_SearchAndFind(t *testing.T) { - repo := NewInmemRepository() - m := NewManager(repo, password.NewFakeService()) - u1 := NewFixtureUser() - u2 := NewFixtureUser() - u2.FirstName = "Lemmy" - - uID, _ := m.Create(u1) - _, _ = m.Create(u2) - - t.Run("search", func(t *testing.T) { - c, err := m.Search("ozzy") - assert.Nil(t, err) - assert.Equal(t, 1, len(c)) - assert.Equal(t, "Osbourne", c[0].LastName) - - c, err = m.Search("dio") - assert.Equal(t, domain.ErrNotFound, err) - assert.Nil(t, c) - }) - t.Run("list all", func(t *testing.T) { - all, err := m.List() - assert.Nil(t, err) - assert.Equal(t, 2, len(all)) - }) - - t.Run("get", func(t *testing.T) { - saved, err := m.Get(uID) - assert.Nil(t, err) - assert.Equal(t, u1.FirstName, saved.FirstName) - }) -} - -func Test_Update(t *testing.T) { - repo := NewInmemRepository() - m := NewManager(repo, password.NewFakeService()) - u := NewFixtureUser() - id, err := m.Create(u) - assert.Nil(t, err) - saved, _ := m.Get(id) - saved.FirstName = "Dio" - saved.Books = append(saved.Books, entity.NewID()) - assert.Nil(t, m.Update(saved)) - updated, err := m.Get(id) - assert.Nil(t, err) - assert.Equal(t, "Dio", updated.FirstName) - assert.False(t, u.UpdatedAt.IsZero()) - assert.Equal(t, 1, len(updated.Books)) -} - -func TestDelete(t *testing.T) { - repo := NewInmemRepository() - m := NewManager(repo, password.NewFakeService()) - u1 := NewFixtureUser() - u2 := NewFixtureUser() - u2ID, _ := m.Create(u2) - - err := m.Delete(u1.ID) - assert.Equal(t, domain.ErrNotFound, err) - - err = m.Delete(u2ID) - assert.Nil(t, err) - _, err = m.Get(u2ID) - assert.Equal(t, domain.ErrNotFound, err) - - u3 := NewFixtureUser() - u3.Books = []entity.ID{entity.NewID()} - _, _ = m.Create(u3) - err = m.Delete(u3.ID) - assert.Equal(t, domain.ErrCannotBeDeleted, err) -} diff --git a/domain/entity/user/repository_inmem.go b/domain/entity/user/repository_inmem.go deleted file mode 100644 index 977689e..0000000 --- a/domain/entity/user/repository_inmem.go +++ /dev/null @@ -1,78 +0,0 @@ -package user - -import ( - "strings" - - "github.com/eminetto/clean-architecture-go-v2/domain" - "github.com/eminetto/clean-architecture-go-v2/domain/entity" -) - -//IRepo in memory repo -type IRepo struct { - m map[entity.ID]*User -} - -//NewInmemRepository create new repository -func NewInmemRepository() *IRepo { - var m = map[entity.ID]*User{} - return &IRepo{ - m: m, - } -} - -//Create an user -func (r *IRepo) Create(e *User) (entity.ID, error) { - r.m[e.ID] = e - return e.ID, nil -} - -//Get an user -func (r *IRepo) Get(id entity.ID) (*User, error) { - if r.m[id] == nil { - return nil, domain.ErrNotFound - } - return r.m[id], nil -} - -//Update an user -func (r *IRepo) Update(e *User) error { - _, err := r.Get(e.ID) - if err != nil { - return err - } - r.m[e.ID] = e - return nil -} - -//Search users -func (r *IRepo) Search(query string) ([]*User, error) { - var d []*User - for _, j := range r.m { - if strings.Contains(strings.ToLower(j.FirstName), query) { - d = append(d, j) - } - } - if len(d) == 0 { - return nil, domain.ErrNotFound - } - - return d, nil -} - -//List users -func (r *IRepo) List() ([]*User, error) { - var d []*User - for _, j := range r.m { - d = append(d, j) - } - return d, nil -} - -//Delete an user -func (r *IRepo) Delete(id entity.ID) error { - if r.m[id] == nil { - return domain.ErrNotFound - } - r.m[id] = nil - return nil -} diff --git a/domain/usecase/loan/interface.go b/domain/usecase/loan/interface.go deleted file mode 100644 index 1ad50b2..0000000 --- a/domain/usecase/loan/interface.go +++ /dev/null @@ -1,12 +0,0 @@ -package loan - -import ( - "github.com/eminetto/clean-architecture-go-v2/domain/entity/book" - "github.com/eminetto/clean-architecture-go-v2/domain/entity/user" -) - -//UseCase use case interface -type UseCase interface { - Borrow(u *user.User, b *book.Book) error - Return(b *book.Book) error -} diff --git a/domain/usecase/loan/usecase.go b/domain/usecase/loan/usecase.go deleted file mode 100644 index 7daa7d0..0000000 --- a/domain/usecase/loan/usecase.go +++ /dev/null @@ -1,100 +0,0 @@ -package loan - -import ( - "github.com/eminetto/clean-architecture-go-v2/domain" - "github.com/eminetto/clean-architecture-go-v2/domain/entity" - "github.com/eminetto/clean-architecture-go-v2/domain/entity/book" - "github.com/eminetto/clean-architecture-go-v2/domain/entity/user" -) - -type usecase struct { - uManager user.Manager - bManager book.Manager -} - -//NewUseCase create new use case -func NewUseCase(u user.Manager, b book.Manager) *usecase { - return &usecase{ - uManager: u, - bManager: b, - } -} - -//Borrow borrow a book to an user -func (s *usecase) Borrow(u *user.User, b *book.Book) error { - u, err := s.uManager.Get(u.ID) - if err != nil { - return err - } - b, err = s.bManager.Get(b.ID) - if err != nil { - return err - } - if b.Quantity <= 0 { - return domain.ErrNotEnoughBooks - } - for _, v := range u.Books { - if v == b.ID { - return domain.ErrBookAlreadyBorrowed - } - } - u.Books = append(u.Books, b.ID) - err = s.uManager.Update(u) - if err != nil { - return err - } - b.Quantity-- - err = s.bManager.Update(b) - if err != nil { - return err - } - return nil -} - -//Return return a book -func (s *usecase) Return(b *book.Book) error { - b, err := s.bManager.Get(b.ID) - if err != nil { - return err - } - - all, err := s.uManager.List() - if err != nil { - return err - } - borrowed := false - var borrowedBy entity.ID - for _, u := range all { - for _, bookID := range u.Books { - if bookID == b.ID { - borrowed = true - borrowedBy = u.ID - break - } - } - } - if !borrowed { - return domain.ErrBookNotBorrowed - } - u, err := s.uManager.Get(borrowedBy) - if err != nil { - return err - } - for i, j := range u.Books { - if j == b.ID { - u.Books = append(u.Books[:i], u.Books[i+1:]...) - err = s.uManager.Update(u) - if err != nil { - return err - } - break - } - } - b.Quantity++ - err = s.bManager.Update(b) - if err != nil { - return err - } - - return nil -} diff --git a/domain/usecase/loan/usecase_test.go b/domain/usecase/loan/usecase_test.go deleted file mode 100644 index 233b02d..0000000 --- a/domain/usecase/loan/usecase_test.go +++ /dev/null @@ -1,101 +0,0 @@ -package loan - -import ( - "testing" - - "github.com/eminetto/clean-architecture-go-v2/domain/entity" - - "github.com/eminetto/clean-architecture-go-v2/domain" - "github.com/eminetto/clean-architecture-go-v2/domain/entity/book" - bmock "github.com/eminetto/clean-architecture-go-v2/domain/entity/book/mock" - "github.com/eminetto/clean-architecture-go-v2/domain/entity/user" - umock "github.com/eminetto/clean-architecture-go-v2/domain/entity/user/mock" - "github.com/golang/mock/gomock" - "github.com/stretchr/testify/assert" -) - -func Test_Borrow(t *testing.T) { - controller := gomock.NewController(t) - defer controller.Finish() - uMock := umock.NewMockManager(controller) - bMock := bmock.NewMockManager(controller) - uc := NewUseCase(uMock, bMock) - t.Run("user not found", func(t *testing.T) { - u := user.NewFixtureUser() - b := book.NewFixtureBook() - uMock.EXPECT().Get(u.ID).Return(nil, domain.ErrNotFound) - err := uc.Borrow(u, b) - assert.Equal(t, domain.ErrNotFound, err) - }) - t.Run("book not found", func(t *testing.T) { - u := user.NewFixtureUser() - b := book.NewFixtureBook() - uMock.EXPECT().Get(u.ID).Return(u, nil) - bMock.EXPECT().Get(b.ID).Return(nil, domain.ErrNotFound) - err := uc.Borrow(u, b) - assert.Equal(t, domain.ErrNotFound, err) - }) - t.Run("not enough books to borrow", func(t *testing.T) { - u := user.NewFixtureUser() - b := book.NewFixtureBook() - b.Quantity = 0 - uMock.EXPECT().Get(u.ID).Return(u, nil) - bMock.EXPECT().Get(b.ID).Return(b, nil) - err := uc.Borrow(u, b) - assert.Equal(t, domain.ErrNotEnoughBooks, err) - }) - t.Run("book already borrowed", func(t *testing.T) { - u := user.NewFixtureUser() - b := book.NewFixtureBook() - u.Books = []entity.ID{b.ID} - b.Quantity = 1 - uMock.EXPECT().Get(u.ID).Return(u, nil) - bMock.EXPECT().Get(b.ID).Return(b, nil) - err := uc.Borrow(u, b) - assert.Equal(t, domain.ErrBookAlreadyBorrowed, err) - }) - t.Run("sucess", func(t *testing.T) { - u := user.NewFixtureUser() - b := book.NewFixtureBook() - uMock.EXPECT().Get(u.ID).Return(u, nil) - bMock.EXPECT().Get(b.ID).Return(b, nil) - uMock.EXPECT().Update(u).Return(nil) - bMock.EXPECT().Update(b).Return(nil) - err := uc.Borrow(u, b) - assert.Nil(t, err) - }) -} - -func Test_Return(t *testing.T) { - controller := gomock.NewController(t) - defer controller.Finish() - uMock := umock.NewMockManager(controller) - bMock := bmock.NewMockManager(controller) - uc := NewUseCase(uMock, bMock) - t.Run("book not found", func(t *testing.T) { - b := book.NewFixtureBook() - bMock.EXPECT().Get(b.ID).Return(nil, domain.ErrNotFound) - err := uc.Return(b) - assert.Equal(t, domain.ErrNotFound, err) - }) - t.Run("book not borrowed", func(t *testing.T) { - u := user.NewFixtureUser() - b := book.NewFixtureBook() - bMock.EXPECT().Get(b.ID).Return(b, nil) - uMock.EXPECT().List().Return([]*user.User{u}, nil) - err := uc.Return(b) - assert.Equal(t, domain.ErrBookNotBorrowed, err) - }) - t.Run("success", func(t *testing.T) { - u := user.NewFixtureUser() - b := book.NewFixtureBook() - u.Books = []entity.ID{b.ID} - bMock.EXPECT().Get(b.ID).Return(b, nil) - uMock.EXPECT().Get(u.ID).Return(u, nil) - uMock.EXPECT().List().Return([]*user.User{u}, nil) - uMock.EXPECT().Update(u).Return(nil) - bMock.EXPECT().Update(b).Return(nil) - err := uc.Return(b) - assert.Nil(t, err) - }) -} diff --git a/domain/entity/book/repository_mysql.go b/driver/repository/book_mysql.go similarity index 72% rename from domain/entity/book/repository_mysql.go rename to driver/repository/book_mysql.go index c46f051..f8ae076 100644 --- a/domain/entity/book/repository_mysql.go +++ b/driver/repository/book_mysql.go @@ -1,28 +1,26 @@ -package book +package repository import ( "database/sql" "time" - "github.com/eminetto/clean-architecture-go-v2/domain" - - "github.com/eminetto/clean-architecture-go-v2/domain/entity" + "github.com/eminetto/clean-architecture-go-v2/entity" ) -//mySQLRepo mysql repo -type mySQLRepo struct { +//BookMySQL mysql repo +type BookMySQL struct { db *sql.DB } -//NewMySQLRepository create new repository -func NewMySQLRepository(db *sql.DB) *mySQLRepo { - return &mySQLRepo{ +//NewBookMySQL create new repository +func NewBookMySQL(db *sql.DB) *BookMySQL { + return &BookMySQL{ db: db, } } //Create a book -func (r *mySQLRepo) Create(e *Book) (entity.ID, error) { +func (r *BookMySQL) Create(e *entity.Book) (entity.ID, error) { stmt, err := r.db.Prepare(` insert into book (id, title, author, pages, quantity, created_at) values(?,?,?,?,?,?)`) @@ -48,12 +46,12 @@ func (r *mySQLRepo) Create(e *Book) (entity.ID, error) { } //Get a book -func (r *mySQLRepo) Get(id entity.ID) (*Book, error) { +func (r *BookMySQL) Get(id entity.ID) (*entity.Book, error) { stmt, err := r.db.Prepare(`select id, title, author, pages, quantity, created_at from book where id = ?`) if err != nil { return nil, err } - var b Book + var b entity.Book rows, err := stmt.Query(id) if err != nil { return nil, err @@ -65,7 +63,7 @@ func (r *mySQLRepo) Get(id entity.ID) (*Book, error) { } //Update a book -func (r *mySQLRepo) Update(e *Book) error { +func (r *BookMySQL) Update(e *entity.Book) error { e.UpdatedAt = time.Now() _, err := r.db.Exec("update book set title = ?, author = ?, pages = ?, quantity = ?, updated_at = ? where id = ?", e.Title, e.Author, e.Pages, e.Quantity, e.UpdatedAt.Format("2006-01-02"), e.ID) if err != nil { @@ -75,57 +73,52 @@ func (r *mySQLRepo) Update(e *Book) error { } //Search books -func (r *mySQLRepo) Search(query string) ([]*Book, error) { +func (r *BookMySQL) Search(query string) ([]*entity.Book, error) { stmt, err := r.db.Prepare(`select id, title, author, pages, quantity, created_at from book where title like ?`) if err != nil { return nil, err } - var books []*Book + var books []*entity.Book rows, err := stmt.Query("%" + query + "%") if err != nil { return nil, err } for rows.Next() { - var b Book + var b entity.Book err = rows.Scan(&b.ID, &b.Title, &b.Author, &b.Pages, &b.Quantity, &b.CreatedAt) if err != nil { return nil, err } books = append(books, &b) } - if len(books) == 0 { - return nil, domain.ErrNotFound - } + return books, nil } //List books -func (r *mySQLRepo) List() ([]*Book, error) { +func (r *BookMySQL) List() ([]*entity.Book, error) { stmt, err := r.db.Prepare(`select id, title, author, pages, quantity, created_at from book`) if err != nil { return nil, err } - var books []*Book + var books []*entity.Book rows, err := stmt.Query() if err != nil { return nil, err } for rows.Next() { - var b Book + var b entity.Book err = rows.Scan(&b.ID, &b.Title, &b.Author, &b.Pages, &b.Quantity, &b.CreatedAt) if err != nil { return nil, err } books = append(books, &b) } - if len(books) == 0 { - return nil, domain.ErrNotFound - } return books, nil } //Delete a book -func (r *mySQLRepo) Delete(id entity.ID) error { +func (r *BookMySQL) Delete(id entity.ID) error { _, err := r.db.Exec("delete from book where id = ?", id) if err != nil { return err diff --git a/domain/entity/user/repository_mysql.go b/driver/repository/user_mysql.go similarity index 78% rename from domain/entity/user/repository_mysql.go rename to driver/repository/user_mysql.go index d9ac700..73cb0bb 100644 --- a/domain/entity/user/repository_mysql.go +++ b/driver/repository/user_mysql.go @@ -1,28 +1,27 @@ -package user +package repository import ( "database/sql" + "fmt" "time" - "github.com/eminetto/clean-architecture-go-v2/domain" - - "github.com/eminetto/clean-architecture-go-v2/domain/entity" + "github.com/eminetto/clean-architecture-go-v2/entity" ) -//MySQLRepo mysql repo -type MySQLRepo struct { +//UserMySQL mysql repo +type UserMySQL struct { db *sql.DB } -//NewMySQLRepoRepository create new repository -func NewMySQLRepoRepository(db *sql.DB) *MySQLRepo { - return &MySQLRepo{ +//NewUserMySQL create new repository +func NewUserMySQL(db *sql.DB) *UserMySQL { + return &UserMySQL{ db: db, } } //Create an user -func (r *MySQLRepo) Create(e *User) (entity.ID, error) { +func (r *UserMySQL) Create(e *entity.User) (entity.ID, error) { stmt, err := r.db.Prepare(` insert into user (id, email, password, first_name, last_name, created_at) values(?,?,?,?,?,?)`) @@ -48,16 +47,16 @@ func (r *MySQLRepo) Create(e *User) (entity.ID, error) { } //Get an user -func (r *MySQLRepo) Get(id entity.ID) (*User, error) { +func (r *UserMySQL) Get(id entity.ID) (*entity.User, error) { return getUser(id, r.db) } -func getUser(id entity.ID, db *sql.DB) (*User, error) { +func getUser(id entity.ID, db *sql.DB) (*entity.User, error) { stmt, err := db.Prepare(`select id, email, first_name, last_name, created_at from user where id = ?`) if err != nil { return nil, err } - var u User + var u entity.User rows, err := stmt.Query(id) if err != nil { return nil, err @@ -82,7 +81,7 @@ func getUser(id entity.ID, db *sql.DB) (*User, error) { } //Update an user -func (r *MySQLRepo) Update(e *User) error { +func (r *UserMySQL) Update(e *entity.User) error { e.UpdatedAt = time.Now() _, err := r.db.Exec("update user set email = ?, password = ?, first_name = ?, last_name = ?, updated_at = ? where id = ?", e.Email, e.Password, e.FirstName, e.LastName, e.UpdatedAt.Format("2006-01-02"), e.ID) if err != nil { @@ -102,7 +101,7 @@ func (r *MySQLRepo) Update(e *User) error { } //Search users -func (r *MySQLRepo) Search(query string) ([]*User, error) { +func (r *UserMySQL) Search(query string) ([]*entity.User, error) { stmt, err := r.db.Prepare(`select id from user where name like ?`) if err != nil { return nil, err @@ -122,9 +121,9 @@ func (r *MySQLRepo) Search(query string) ([]*User, error) { ids = append(ids, i) } if len(ids) == 0 { - return nil, domain.ErrNotFound + return nil, fmt.Errorf("not found") } - var users []*User + var users []*entity.User for _, id := range ids { u, err := getUser(id, r.db) if err != nil { @@ -136,7 +135,7 @@ func (r *MySQLRepo) Search(query string) ([]*User, error) { } //List users -func (r *MySQLRepo) List() ([]*User, error) { +func (r *UserMySQL) List() ([]*entity.User, error) { stmt, err := r.db.Prepare(`select id from user`) if err != nil { return nil, err @@ -156,9 +155,9 @@ func (r *MySQLRepo) List() ([]*User, error) { ids = append(ids, i) } if len(ids) == 0 { - return nil, domain.ErrNotFound + return nil, fmt.Errorf("not found") } - var users []*User + var users []*entity.User for _, id := range ids { u, err := getUser(id, r.db) if err != nil { @@ -170,7 +169,7 @@ func (r *MySQLRepo) List() ([]*User, error) { } //Delete an user -func (r *MySQLRepo) Delete(id entity.ID) error { +func (r *UserMySQL) Delete(id entity.ID) error { _, err := r.db.Exec("delete from user where id = ?", id) if err != nil { return err diff --git a/entity/book.go b/entity/book.go new file mode 100644 index 0000000..87b9c24 --- /dev/null +++ b/entity/book.go @@ -0,0 +1,41 @@ +package entity + +import ( + "time" +) + +//Book data +type Book struct { + ID ID + Title string + Author string + Pages int + Quantity int + CreatedAt time.Time + UpdatedAt time.Time +} + +//NewBook create a new book +func NewBook(title string, author string, pages int, quantity int) (*Book, error) { + b := &Book{ + ID: NewID(), + Title: title, + Author: author, + Pages: pages, + Quantity: quantity, + CreatedAt: time.Now(), + } + err := b.Validate() + if err != nil { + return nil, ErrInvalidEntity + } + return b, nil +} + +//Validate validate book +func (b *Book) Validate() error { + if b.Title == "" || b.Author == "" || b.Pages <= 0 || b.Quantity <= 0 { + return ErrInvalidEntity + } + return nil +} diff --git a/entity/book_test.go b/entity/book_test.go new file mode 100644 index 0000000..a77a33d --- /dev/null +++ b/entity/book_test.go @@ -0,0 +1,69 @@ +package entity_test + +import ( + "testing" + + "github.com/eminetto/clean-architecture-go-v2/entity" + "github.com/stretchr/testify/assert" +) + +func TestNewBook(t *testing.T) { + b, err := entity.NewBook("American Gods", "Neil Gaiman", 100, 1) + assert.Nil(t, err) + assert.Equal(t, b.Title, "American Gods") + assert.NotNil(t, b.ID) +} + +func TestBookValidate(t *testing.T) { + type test struct { + title string + author string + pages int + quantity int + want error + } + + tests := []test{ + { + title: "American Gods", + author: "Neil Gaiman", + pages: 100, + quantity: 1, + want: nil, + }, + { + title: "American Gods", + author: "Neil Gaiman", + pages: 100, + quantity: 0, + want: entity.ErrInvalidEntity, + }, + { + title: "", + author: "Neil Gaiman", + pages: 100, + quantity: 1, + want: entity.ErrInvalidEntity, + }, + { + title: "American Gods", + author: "", + pages: 100, + quantity: 1, + want: entity.ErrInvalidEntity, + }, + { + title: "American Gods", + author: "Neil Gaiman", + pages: 0, + quantity: 1, + want: entity.ErrInvalidEntity, + }, + } + for _, tc := range tests { + + _, err := entity.NewBook(tc.title, tc.author, tc.pages, tc.quantity) + assert.Equal(t, err, tc.want) + } + +} diff --git a/domain/entity/entity.go b/entity/entity.go similarity index 68% rename from domain/entity/entity.go rename to entity/entity.go index 714caf8..9c99501 100644 --- a/domain/entity/entity.go +++ b/entity/entity.go @@ -2,15 +2,16 @@ package entity import "github.com/google/uuid" +//ID entity ID type ID = uuid.UUID +//NewID create a new entity ID func NewID() ID { return ID(uuid.New()) } +//StringToID convert a string to an entity ID func StringToID(s string) (ID, error) { id, err := uuid.Parse(s) return ID(id), err } - - diff --git a/domain/error.go b/entity/error.go similarity index 82% rename from domain/error.go rename to entity/error.go index e235ef3..f63c9d1 100644 --- a/domain/error.go +++ b/entity/error.go @@ -1,10 +1,13 @@ -package domain +package entity import "errors" //ErrNotFound not found var ErrNotFound = errors.New("Not found") +//ErrInvalidEntity invalid entity +var ErrInvalidEntity = errors.New("Invalid entity") + //ErrCannotBeDeleted cannot be deleted var ErrCannotBeDeleted = errors.New("Cannot Be Deleted") diff --git a/entity/user.go b/entity/user.go new file mode 100644 index 0000000..fbb8a9e --- /dev/null +++ b/entity/user.go @@ -0,0 +1,97 @@ +package entity + +import ( + "time" + + "golang.org/x/crypto/bcrypt" +) + +//User data +type User struct { + ID ID + Email string + Password string + FirstName string + LastName string + CreatedAt time.Time + UpdatedAt time.Time + Books []ID +} + +//NewUser create a new user +func NewUser(email, password, firstName, lastName string) (*User, error) { + u := &User{ + ID: NewID(), + Email: email, + FirstName: firstName, + LastName: lastName, + CreatedAt: time.Now(), + } + pwd, err := generatePassword(password) + if err != nil { + return nil, err + } + u.Password = pwd + err = u.Validate() + if err != nil { + return nil, ErrInvalidEntity + } + return u, nil +} + +//AddBook add a book +func (u *User) AddBook(id ID) error { + _, err := u.GetBook(id) + if err == nil { + return ErrBookAlreadyBorrowed + } + u.Books = append(u.Books, id) + return nil +} + +//RemoveBook remove a book +func (u *User) RemoveBook(id ID) error { + for i, j := range u.Books { + if j == id { + u.Books = append(u.Books[:i], u.Books[i+1:]...) + return nil + } + } + return ErrNotFound +} + +//GetBook get a book +func (u *User) GetBook(id ID) (ID, error) { + for _, v := range u.Books { + if v == id { + return id, nil + } + } + return id, ErrNotFound +} + +//Validate validate data +func (u *User) Validate() error { + if u.Email == "" || u.FirstName == "" || u.LastName == "" || u.Password == "" { + return ErrInvalidEntity + } + + return nil +} + +//ValidatePassword validate user password +func (u *User) ValidatePassword(p string) error { + err := bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(p)) + if err != nil { + return err + } + return nil +} + +func generatePassword(raw string) (string, error) { + hash, err := bcrypt.GenerateFromPassword([]byte(raw), 10) + if err != nil { + return "", err + } + return string(hash), nil +} diff --git a/entity/user_test.go b/entity/user_test.go new file mode 100644 index 0000000..01d4f76 --- /dev/null +++ b/entity/user_test.go @@ -0,0 +1,111 @@ +package entity_test + +import ( + "testing" + + "github.com/eminetto/clean-architecture-go-v2/entity" + "github.com/stretchr/testify/assert" +) + +func TestNewUser(t *testing.T) { + u, err := entity.NewUser("sjobs@apple.com", "new_password", "Steve", "Jobs") + assert.Nil(t, err) + assert.Equal(t, u.FirstName, "Steve") + assert.NotNil(t, u.ID) + assert.NotEqual(t, u.Password, "new_password") +} + +func TestValidatePassword(t *testing.T) { + u, _ := entity.NewUser("sjobs@apple.com", "new_password", "Steve", "Jobs") + err := u.ValidatePassword("new_password") + assert.Nil(t, err) + err = u.ValidatePassword("wrong_password") + assert.NotNil(t, err) + +} + +func TestAddBook(t *testing.T) { + u, _ := entity.NewUser("sjobs@apple.com", "new_password", "Steve", "Jobs") + bID := entity.NewID() + err := u.AddBook(bID) + assert.Nil(t, err) + assert.Equal(t, 1, len(u.Books)) + err = u.AddBook(bID) + assert.Equal(t, entity.ErrBookAlreadyBorrowed, err) + +} + +func TestRemoveBook(t *testing.T) { + u, _ := entity.NewUser("sjobs@apple.com", "new_password", "Steve", "Jobs") + err := u.RemoveBook(entity.NewID()) + assert.Equal(t, entity.ErrNotFound, err) + bID := entity.NewID() + _ = u.AddBook(bID) + err = u.RemoveBook(bID) + assert.Nil(t, err) +} + +func TestGetBook(t *testing.T) { + u, _ := entity.NewUser("sjobs@apple.com", "new_password", "Steve", "Jobs") + bID := entity.NewID() + _ = u.AddBook(bID) + id, err := u.GetBook(bID) + assert.Nil(t, err) + assert.Equal(t, id, bID) + _, err = u.GetBook(entity.NewID()) + assert.Equal(t, entity.ErrNotFound, err) +} + +func TestUserValidate(t *testing.T) { + type test struct { + email string + password string + firstName string + lastName string + want error + } + + tests := []test{ + { + email: "sjobs@apple.com", + password: "new_password", + firstName: "Steve", + lastName: "Jobs", + want: nil, + }, + { + email: "", + password: "new_password", + firstName: "Steve", + lastName: "Jobs", + want: entity.ErrInvalidEntity, + }, + { + email: "sjobs@apple.com", + password: "", + firstName: "Steve", + lastName: "Jobs", + want: nil, + }, + { + email: "sjobs@apple.com", + password: "new_password", + firstName: "", + lastName: "Jobs", + want: entity.ErrInvalidEntity, + }, + { + email: "sjobs@apple.com", + password: "new_password", + firstName: "Steve", + lastName: "", + want: entity.ErrInvalidEntity, + }, + } + for _, tc := range tests { + + _, err := entity.NewUser(tc.email, tc.password, tc.firstName, tc.lastName) + assert.Equal(t, err, tc.want) + } + +} diff --git a/go.mod b/go.mod index 36aa8a5..0faee33 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/codegangsta/negroni v0.3.0 github.com/eminetto/clean-architecture-go v0.0.0-20200312132632-3195811c74a3 github.com/go-sql-driver/mysql v1.5.0 - github.com/golang/mock v1.4.3 + github.com/golang/mock v1.4.4 github.com/google/uuid v1.1.1 github.com/gorilla/context v1.1.1 github.com/gorilla/mux v1.6.2 @@ -17,4 +17,5 @@ require ( github.com/stretchr/testify v1.6.1 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce + rsc.io/quote/v3 v3.1.0 // indirect ) diff --git a/go.sum b/go.sum index 23e5870..ffbed44 100644 --- a/go.sum +++ b/go.sum @@ -32,6 +32,8 @@ github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7a github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/mock v1.4.3 h1:GV+pQPG/EUUbkh47niozDcADz6go/dUwhVzdUQHIVRw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= diff --git a/infra/init.sql b/ops/db/init.sql similarity index 100% rename from infra/init.sql rename to ops/db/init.sql diff --git a/infra/prometheus/prometheus.yml b/ops/prometheus/prometheus.yml similarity index 100% rename from infra/prometheus/prometheus.yml rename to ops/prometheus/prometheus.yml diff --git a/usecase/book/inmem.go b/usecase/book/inmem.go new file mode 100644 index 0000000..c9c9e5e --- /dev/null +++ b/usecase/book/inmem.go @@ -0,0 +1,73 @@ +package book + +import ( + "strings" + + "github.com/eminetto/clean-architecture-go-v2/entity" +) + +//inmem in memory repo +type inmem struct { + m map[entity.ID]*entity.Book +} + +//newInmem create new repository +func newInmem() *inmem { + var m = map[entity.ID]*entity.Book{} + return &inmem{ + m: m, + } +} + +//Create a book +func (r *inmem) Create(e *entity.Book) (entity.ID, error) { + r.m[e.ID] = e + return e.ID, nil +} + +//Get a book +func (r *inmem) Get(id entity.ID) (*entity.Book, error) { + if r.m[id] == nil { + return nil, entity.ErrNotFound + } + return r.m[id], nil +} + +//Update a book +func (r *inmem) Update(e *entity.Book) error { + _, err := r.Get(e.ID) + if err != nil { + return err + } + r.m[e.ID] = e + return nil +} + +//Search books +func (r *inmem) Search(query string) ([]*entity.Book, error) { + var d []*entity.Book + for _, j := range r.m { + if strings.Contains(strings.ToLower(j.Title), query) { + d = append(d, j) + } + } + return d, nil +} + +//List books +func (r *inmem) List() ([]*entity.Book, error) { + var d []*entity.Book + for _, j := range r.m { + d = append(d, j) + } + return d, nil +} + +//Delete a book +func (r *inmem) Delete(id entity.ID) error { + if r.m[id] == nil { + return entity.ErrNotFound + } + r.m[id] = nil + return nil +} diff --git a/usecase/book/interface.go b/usecase/book/interface.go new file mode 100644 index 0000000..3be17a6 --- /dev/null +++ b/usecase/book/interface.go @@ -0,0 +1,35 @@ +package book + +import ( + "github.com/eminetto/clean-architecture-go-v2/entity" +) + +//Reader interface +type Reader interface { + Get(id entity.ID) (*entity.Book, error) + Search(query string) ([]*entity.Book, error) + List() ([]*entity.Book, error) +} + +//Writer book writer +type Writer interface { + Create(e *entity.Book) (entity.ID, error) + Update(e *entity.Book) error + Delete(id entity.ID) error +} + +//Repository interface +type Repository interface { + Reader + Writer +} + +//UseCase interface +type UseCase interface { + GetBook(id entity.ID) (*entity.Book, error) + SearchBooks(query string) ([]*entity.Book, error) + ListBooks() ([]*entity.Book, error) + CreateBook(title string, author string, pages int, quantity int) (entity.ID, error) + UpdateBook(e *entity.Book) error + DeleteBook(id entity.ID) error +} diff --git a/domain/entity/book/mock/book.go b/usecase/book/mock/book.go similarity index 66% rename from domain/entity/book/mock/book.go rename to usecase/book/mock/book.go index e213511..86920a0 100644 --- a/domain/entity/book/mock/book.go +++ b/usecase/book/mock/book.go @@ -1,12 +1,11 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: domain/entity/book/interface.go +// Source: usecase/book/interface.go // Package mock is a generated GoMock package. package mock import ( - entity "github.com/eminetto/clean-architecture-go-v2/domain/entity" - book "github.com/eminetto/clean-architecture-go-v2/domain/entity/book" + entity "github.com/eminetto/clean-architecture-go-v2/entity" gomock "github.com/golang/mock/gomock" reflect "reflect" ) @@ -35,10 +34,10 @@ func (m *MockReader) EXPECT() *MockReaderMockRecorder { } // Get mocks base method -func (m *MockReader) Get(id entity.ID) (*book.Book, error) { +func (m *MockReader) Get(id entity.ID) (*entity.Book, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Get", id) - ret0, _ := ret[0].(*book.Book) + ret0, _ := ret[0].(*entity.Book) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -50,10 +49,10 @@ func (mr *MockReaderMockRecorder) Get(id interface{}) *gomock.Call { } // Search mocks base method -func (m *MockReader) Search(query string) ([]*book.Book, error) { +func (m *MockReader) Search(query string) ([]*entity.Book, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Search", query) - ret0, _ := ret[0].([]*book.Book) + ret0, _ := ret[0].([]*entity.Book) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -65,10 +64,10 @@ func (mr *MockReaderMockRecorder) Search(query interface{}) *gomock.Call { } // List mocks base method -func (m *MockReader) List() ([]*book.Book, error) { +func (m *MockReader) List() ([]*entity.Book, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "List") - ret0, _ := ret[0].([]*book.Book) + ret0, _ := ret[0].([]*entity.Book) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -103,7 +102,7 @@ func (m *MockWriter) EXPECT() *MockWriterMockRecorder { } // Create mocks base method -func (m *MockWriter) Create(e *book.Book) (entity.ID, error) { +func (m *MockWriter) Create(e *entity.Book) (entity.ID, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Create", e) ret0, _ := ret[0].(entity.ID) @@ -118,7 +117,7 @@ func (mr *MockWriterMockRecorder) Create(e interface{}) *gomock.Call { } // Update mocks base method -func (m *MockWriter) Update(e *book.Book) error { +func (m *MockWriter) Update(e *entity.Book) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Update", e) ret0, _ := ret[0].(error) @@ -169,10 +168,10 @@ func (m *MockRepository) EXPECT() *MockRepositoryMockRecorder { } // Get mocks base method -func (m *MockRepository) Get(id entity.ID) (*book.Book, error) { +func (m *MockRepository) Get(id entity.ID) (*entity.Book, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Get", id) - ret0, _ := ret[0].(*book.Book) + ret0, _ := ret[0].(*entity.Book) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -184,10 +183,10 @@ func (mr *MockRepositoryMockRecorder) Get(id interface{}) *gomock.Call { } // Search mocks base method -func (m *MockRepository) Search(query string) ([]*book.Book, error) { +func (m *MockRepository) Search(query string) ([]*entity.Book, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Search", query) - ret0, _ := ret[0].([]*book.Book) + ret0, _ := ret[0].([]*entity.Book) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -199,10 +198,10 @@ func (mr *MockRepositoryMockRecorder) Search(query interface{}) *gomock.Call { } // List mocks base method -func (m *MockRepository) List() ([]*book.Book, error) { +func (m *MockRepository) List() ([]*entity.Book, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "List") - ret0, _ := ret[0].([]*book.Book) + ret0, _ := ret[0].([]*entity.Book) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -214,7 +213,7 @@ func (mr *MockRepositoryMockRecorder) List() *gomock.Call { } // Create mocks base method -func (m *MockRepository) Create(e *book.Book) (entity.ID, error) { +func (m *MockRepository) Create(e *entity.Book) (entity.ID, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Create", e) ret0, _ := ret[0].(entity.ID) @@ -229,7 +228,7 @@ func (mr *MockRepositoryMockRecorder) Create(e interface{}) *gomock.Call { } // Update mocks base method -func (m *MockRepository) Update(e *book.Book) error { +func (m *MockRepository) Update(e *entity.Book) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Update", e) ret0, _ := ret[0].(error) @@ -256,113 +255,113 @@ func (mr *MockRepositoryMockRecorder) Delete(id interface{}) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockRepository)(nil).Delete), id) } -// MockManager is a mock of Manager interface -type MockManager struct { +// MockUseCase is a mock of UseCase interface +type MockUseCase struct { ctrl *gomock.Controller - recorder *MockManagerMockRecorder + recorder *MockUseCaseMockRecorder } -// MockManagerMockRecorder is the mock recorder for MockManager -type MockManagerMockRecorder struct { - mock *MockManager +// MockUseCaseMockRecorder is the mock recorder for MockUseCase +type MockUseCaseMockRecorder struct { + mock *MockUseCase } -// NewMockManager creates a new mock instance -func NewMockManager(ctrl *gomock.Controller) *MockManager { - mock := &MockManager{ctrl: ctrl} - mock.recorder = &MockManagerMockRecorder{mock} +// NewMockUseCase creates a new mock instance +func NewMockUseCase(ctrl *gomock.Controller) *MockUseCase { + mock := &MockUseCase{ctrl: ctrl} + mock.recorder = &MockUseCaseMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use -func (m *MockManager) EXPECT() *MockManagerMockRecorder { +func (m *MockUseCase) EXPECT() *MockUseCaseMockRecorder { return m.recorder } -// Get mocks base method -func (m *MockManager) Get(id entity.ID) (*book.Book, error) { +// GetBook mocks base method +func (m *MockUseCase) GetBook(id entity.ID) (*entity.Book, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Get", id) - ret0, _ := ret[0].(*book.Book) + ret := m.ctrl.Call(m, "GetBook", id) + ret0, _ := ret[0].(*entity.Book) ret1, _ := ret[1].(error) return ret0, ret1 } -// Get indicates an expected call of Get -func (mr *MockManagerMockRecorder) Get(id interface{}) *gomock.Call { +// GetBook indicates an expected call of GetBook +func (mr *MockUseCaseMockRecorder) GetBook(id interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockManager)(nil).Get), id) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBook", reflect.TypeOf((*MockUseCase)(nil).GetBook), id) } -// Search mocks base method -func (m *MockManager) Search(query string) ([]*book.Book, error) { +// SearchBooks mocks base method +func (m *MockUseCase) SearchBooks(query string) ([]*entity.Book, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Search", query) - ret0, _ := ret[0].([]*book.Book) + ret := m.ctrl.Call(m, "SearchBooks", query) + ret0, _ := ret[0].([]*entity.Book) ret1, _ := ret[1].(error) return ret0, ret1 } -// Search indicates an expected call of Search -func (mr *MockManagerMockRecorder) Search(query interface{}) *gomock.Call { +// SearchBooks indicates an expected call of SearchBooks +func (mr *MockUseCaseMockRecorder) SearchBooks(query interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Search", reflect.TypeOf((*MockManager)(nil).Search), query) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchBooks", reflect.TypeOf((*MockUseCase)(nil).SearchBooks), query) } -// List mocks base method -func (m *MockManager) List() ([]*book.Book, error) { +// ListBooks mocks base method +func (m *MockUseCase) ListBooks() ([]*entity.Book, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "List") - ret0, _ := ret[0].([]*book.Book) + ret := m.ctrl.Call(m, "ListBooks") + ret0, _ := ret[0].([]*entity.Book) ret1, _ := ret[1].(error) return ret0, ret1 } -// List indicates an expected call of List -func (mr *MockManagerMockRecorder) List() *gomock.Call { +// ListBooks indicates an expected call of ListBooks +func (mr *MockUseCaseMockRecorder) ListBooks() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockManager)(nil).List)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListBooks", reflect.TypeOf((*MockUseCase)(nil).ListBooks)) } -// Create mocks base method -func (m *MockManager) Create(e *book.Book) (entity.ID, error) { +// CreateBook mocks base method +func (m *MockUseCase) CreateBook(title, author string, pages, quantity int) (entity.ID, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Create", e) + ret := m.ctrl.Call(m, "CreateBook", title, author, pages, quantity) ret0, _ := ret[0].(entity.ID) ret1, _ := ret[1].(error) return ret0, ret1 } -// Create indicates an expected call of Create -func (mr *MockManagerMockRecorder) Create(e interface{}) *gomock.Call { +// CreateBook indicates an expected call of CreateBook +func (mr *MockUseCaseMockRecorder) CreateBook(title, author, pages, quantity interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockManager)(nil).Create), e) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateBook", reflect.TypeOf((*MockUseCase)(nil).CreateBook), title, author, pages, quantity) } -// Update mocks base method -func (m *MockManager) Update(e *book.Book) error { +// UpdateBook mocks base method +func (m *MockUseCase) UpdateBook(e *entity.Book) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Update", e) + ret := m.ctrl.Call(m, "UpdateBook", e) ret0, _ := ret[0].(error) return ret0 } -// Update indicates an expected call of Update -func (mr *MockManagerMockRecorder) Update(e interface{}) *gomock.Call { +// UpdateBook indicates an expected call of UpdateBook +func (mr *MockUseCaseMockRecorder) UpdateBook(e interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockManager)(nil).Update), e) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateBook", reflect.TypeOf((*MockUseCase)(nil).UpdateBook), e) } -// Delete mocks base method -func (m *MockManager) Delete(id entity.ID) error { +// DeleteBook mocks base method +func (m *MockUseCase) DeleteBook(id entity.ID) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Delete", id) + ret := m.ctrl.Call(m, "DeleteBook", id) ret0, _ := ret[0].(error) return ret0 } -// Delete indicates an expected call of Delete -func (mr *MockManagerMockRecorder) Delete(id interface{}) *gomock.Call { +// DeleteBook indicates an expected call of DeleteBook +func (mr *MockUseCaseMockRecorder) DeleteBook(id interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockManager)(nil).Delete), id) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBook", reflect.TypeOf((*MockUseCase)(nil).DeleteBook), id) } diff --git a/usecase/book/service.go b/usecase/book/service.go new file mode 100644 index 0000000..a0ebb4e --- /dev/null +++ b/usecase/book/service.go @@ -0,0 +1,85 @@ +package book + +import ( + "strings" + "time" + + "github.com/eminetto/clean-architecture-go-v2/entity" +) + +//Service book usecase +type Service struct { + repo Repository +} + +//NewService create new service +func NewService(r Repository) *Service { + return &Service{ + repo: r, + } +} + +//CreateBook create a book +func (s *Service) CreateBook(title string, author string, pages int, quantity int) (entity.ID, error) { + b, err := entity.NewBook(title, author, pages, quantity) + if err != nil { + return b.ID, err + } + return s.repo.Create(b) +} + +//GetBook get a book +func (s *Service) GetBook(id entity.ID) (*entity.Book, error) { + b, err := s.repo.Get(id) + if b == nil { + return nil, entity.ErrNotFound + } + if err != nil { + return nil, err + } + + return b, nil +} + +//SearchBooks search books +func (s *Service) SearchBooks(query string) ([]*entity.Book, error) { + books, err := s.repo.Search(strings.ToLower(query)) + if err != nil { + return nil, err + } + if len(books) == 0 { + return nil, entity.ErrNotFound + } + return books, nil +} + +//ListBooks list books +func (s *Service) ListBooks() ([]*entity.Book, error) { + books, err := s.repo.List() + if err != nil { + return nil, err + } + if len(books) == 0 { + return nil, entity.ErrNotFound + } + return books, nil +} + +//DeleteBook Delete a book +func (s *Service) DeleteBook(id entity.ID) error { + _, err := s.GetBook(id) + if err != nil { + return err + } + return s.repo.Delete(id) +} + +//UpdateBook Update a book +func (s *Service) UpdateBook(e *entity.Book) error { + err := e.Validate() + if err != nil { + return err + } + e.UpdatedAt = time.Now() + return s.repo.Update(e) +} diff --git a/usecase/book/service_test.go b/usecase/book/service_test.go new file mode 100644 index 0000000..999b5b6 --- /dev/null +++ b/usecase/book/service_test.go @@ -0,0 +1,92 @@ +package book + +import ( + "testing" + "time" + + "github.com/eminetto/clean-architecture-go-v2/entity" + + "github.com/stretchr/testify/assert" +) + +func newFixtureBook() *entity.Book { + return &entity.Book{ + Title: "I Am Ozzy", + Author: "Ozzy Osbourne", + Pages: 294, + Quantity: 1, + CreatedAt: time.Now(), + } +} + +func Test_Create(t *testing.T) { + repo := newInmem() + m := NewService(repo) + u := newFixtureBook() + _, err := m.CreateBook(u.Title, u.Author, u.Pages, u.Quantity) + assert.Nil(t, err) + assert.False(t, u.CreatedAt.IsZero()) +} + +func Test_SearchAndFind(t *testing.T) { + repo := newInmem() + m := NewService(repo) + u1 := newFixtureBook() + u2 := newFixtureBook() + u2.Title = "Lemmy: Biography" + + uID, _ := m.CreateBook(u1.Title, u1.Author, u1.Pages, u1.Quantity) + _, _ = m.CreateBook(u2.Title, u2.Author, u2.Pages, u2.Quantity) + + t.Run("search", func(t *testing.T) { + c, err := m.SearchBooks("ozzy") + assert.Nil(t, err) + assert.Equal(t, 1, len(c)) + assert.Equal(t, "I Am Ozzy", c[0].Title) + + c, err = m.SearchBooks("dio") + assert.Equal(t, entity.ErrNotFound, err) + assert.Nil(t, c) + }) + t.Run("list all", func(t *testing.T) { + all, err := m.ListBooks() + assert.Nil(t, err) + assert.Equal(t, 2, len(all)) + }) + + t.Run("get", func(t *testing.T) { + saved, err := m.GetBook(uID) + assert.Nil(t, err) + assert.Equal(t, u1.Title, saved.Title) + }) +} + +func Test_Update(t *testing.T) { + repo := newInmem() + m := NewService(repo) + u := newFixtureBook() + id, err := m.CreateBook(u.Title, u.Author, u.Pages, u.Quantity) + assert.Nil(t, err) + saved, _ := m.GetBook(id) + saved.Title = "Lemmy: Biography" + assert.Nil(t, m.UpdateBook(saved)) + updated, err := m.GetBook(id) + assert.Nil(t, err) + assert.Equal(t, "Lemmy: Biography", updated.Title) +} + +func TestDelete(t *testing.T) { + repo := newInmem() + m := NewService(repo) + u1 := newFixtureBook() + u2 := newFixtureBook() + u2ID, _ := m.CreateBook(u2.Title, u2.Author, u2.Pages, u2.Quantity) + + err := m.DeleteBook(u1.ID) + assert.Equal(t, entity.ErrNotFound, err) + + err = m.DeleteBook(u2ID) + assert.Nil(t, err) + _, err = m.GetBook(u2ID) + assert.Equal(t, entity.ErrNotFound, err) +} diff --git a/usecase/loan/interface.go b/usecase/loan/interface.go new file mode 100644 index 0000000..802a6ef --- /dev/null +++ b/usecase/loan/interface.go @@ -0,0 +1,11 @@ +package loan + +import ( + "github.com/eminetto/clean-architecture-go-v2/entity" +) + +//UseCase use case interface +type UseCase interface { + Borrow(u *entity.User, b *entity.Book) error + Return(b *entity.Book) error +} diff --git a/domain/usecase/loan/mock/loan.go b/usecase/loan/mock/loan.go similarity index 83% rename from domain/usecase/loan/mock/loan.go rename to usecase/loan/mock/loan.go index ce46511..d3518a0 100644 --- a/domain/usecase/loan/mock/loan.go +++ b/usecase/loan/mock/loan.go @@ -1,12 +1,11 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: domain/usecase/loan/interface.go +// Source: usecase/loan/interface.go // Package mock is a generated GoMock package. package mock import ( - book "github.com/eminetto/clean-architecture-go-v2/domain/entity/book" - user "github.com/eminetto/clean-architecture-go-v2/domain/entity/user" + entity "github.com/eminetto/clean-architecture-go-v2/entity" gomock "github.com/golang/mock/gomock" reflect "reflect" ) @@ -35,7 +34,7 @@ func (m *MockUseCase) EXPECT() *MockUseCaseMockRecorder { } // Borrow mocks base method -func (m *MockUseCase) Borrow(u *user.User, b *book.Book) error { +func (m *MockUseCase) Borrow(u *entity.User, b *entity.Book) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Borrow", u, b) ret0, _ := ret[0].(error) @@ -49,7 +48,7 @@ func (mr *MockUseCaseMockRecorder) Borrow(u, b interface{}) *gomock.Call { } // Return mocks base method -func (m *MockUseCase) Return(b *book.Book) error { +func (m *MockUseCase) Return(b *entity.Book) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Return", b) ret0, _ := ret[0].(error) diff --git a/usecase/loan/service.go b/usecase/loan/service.go new file mode 100644 index 0000000..7516333 --- /dev/null +++ b/usecase/loan/service.go @@ -0,0 +1,97 @@ +package loan + +import ( + "github.com/eminetto/clean-architecture-go-v2/entity" + "github.com/eminetto/clean-architecture-go-v2/usecase/book" + "github.com/eminetto/clean-architecture-go-v2/usecase/user" +) + +//Service loan usecase +type Service struct { + userService user.UseCase + bookService book.UseCase +} + +//NewService create new use case +func NewService(u user.UseCase, b book.UseCase) *Service { + return &Service{ + userService: u, + bookService: b, + } +} + +//Borrow borrow a book to an user +func (s *Service) Borrow(u *entity.User, b *entity.Book) error { + u, err := s.userService.GetUser(u.ID) + if err != nil { + return err + } + b, err = s.bookService.GetBook(b.ID) + if err != nil { + return err + } + if b.Quantity <= 0 { + return entity.ErrNotEnoughBooks + } + + err = u.AddBook(b.ID) + if err != nil { + return err + } + err = s.userService.UpdateUser(u) + if err != nil { + return err + } + b.Quantity-- + err = s.bookService.UpdateBook(b) + if err != nil { + return err + } + return nil +} + +//Return return a book +func (s *Service) Return(b *entity.Book) error { + b, err := s.bookService.GetBook(b.ID) + if err != nil { + return err + } + + all, err := s.userService.ListUsers() + if err != nil { + return err + } + borrowed := false + var borrowedBy entity.ID + for _, u := range all { + _, err := u.GetBook(b.ID) + if err != nil { + continue + } + borrowed = true + borrowedBy = u.ID + break + } + if !borrowed { + return entity.ErrBookNotBorrowed + } + u, err := s.userService.GetUser(borrowedBy) + if err != nil { + return err + } + err = u.RemoveBook(b.ID) + if err != nil { + return err + } + err = s.userService.UpdateUser(u) + if err != nil { + return err + } + b.Quantity++ + err = s.bookService.UpdateBook(b) + if err != nil { + return err + } + + return nil +} diff --git a/usecase/loan/service_test.go b/usecase/loan/service_test.go new file mode 100644 index 0000000..68f30fd --- /dev/null +++ b/usecase/loan/service_test.go @@ -0,0 +1,129 @@ +package loan + +import ( + "testing" + + "github.com/eminetto/clean-architecture-go-v2/entity" + + bmock "github.com/eminetto/clean-architecture-go-v2/usecase/book/mock" + umock "github.com/eminetto/clean-architecture-go-v2/usecase/user/mock" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" +) + +func Test_Borrow(t *testing.T) { + controller := gomock.NewController(t) + defer controller.Finish() + uMock := umock.NewMockUseCase(controller) + bMock := bmock.NewMockUseCase(controller) + uc := NewService(uMock, bMock) + t.Run("user not found", func(t *testing.T) { + u := &entity.User{ + ID: entity.NewID(), + } + b := &entity.Book{ + ID: entity.NewID(), + } + uMock.EXPECT().GetUser(u.ID).Return(nil, entity.ErrNotFound) + err := uc.Borrow(u, b) + assert.Equal(t, entity.ErrNotFound, err) + }) + t.Run("book not found", func(t *testing.T) { + u := &entity.User{ + ID: entity.NewID(), + } + b := &entity.Book{ + ID: entity.NewID(), + } + uMock.EXPECT().GetUser(u.ID).Return(u, nil) + bMock.EXPECT().GetBook(b.ID).Return(nil, entity.ErrNotFound) + err := uc.Borrow(u, b) + assert.Equal(t, entity.ErrNotFound, err) + }) + t.Run("not enough books to borrow", func(t *testing.T) { + u := &entity.User{ + ID: entity.NewID(), + } + b := &entity.Book{ + ID: entity.NewID(), + } + b.Quantity = 0 + uMock.EXPECT().GetUser(u.ID).Return(u, nil) + bMock.EXPECT().GetBook(b.ID).Return(b, nil) + err := uc.Borrow(u, b) + assert.Equal(t, entity.ErrNotEnoughBooks, err) + }) + t.Run("book already borrowed", func(t *testing.T) { + u := &entity.User{ + ID: entity.NewID(), + } + b := &entity.Book{ + ID: entity.NewID(), + } + u.AddBook(b.ID) + b.Quantity = 1 + uMock.EXPECT().GetUser(u.ID).Return(u, nil) + bMock.EXPECT().GetBook(b.ID).Return(b, nil) + err := uc.Borrow(u, b) + assert.Equal(t, entity.ErrBookAlreadyBorrowed, err) + }) + t.Run("sucess", func(t *testing.T) { + u := &entity.User{ + ID: entity.NewID(), + } + b := &entity.Book{ + ID: entity.NewID(), + Quantity: 10, + } + uMock.EXPECT().GetUser(u.ID).Return(u, nil) + bMock.EXPECT().GetBook(b.ID).Return(b, nil) + uMock.EXPECT().UpdateUser(u).Return(nil) + bMock.EXPECT().UpdateBook(b).Return(nil) + err := uc.Borrow(u, b) + assert.Nil(t, err) + }) +} + +func Test_Return(t *testing.T) { + controller := gomock.NewController(t) + defer controller.Finish() + uMock := umock.NewMockUseCase(controller) + bMock := bmock.NewMockUseCase(controller) + uc := NewService(uMock, bMock) + t.Run("book not found", func(t *testing.T) { + b := &entity.Book{ + ID: entity.NewID(), + } + bMock.EXPECT().GetBook(b.ID).Return(nil, entity.ErrNotFound) + err := uc.Return(b) + assert.Equal(t, entity.ErrNotFound, err) + }) + t.Run("book not borrowed", func(t *testing.T) { + u := &entity.User{ + ID: entity.NewID(), + } + b := &entity.Book{ + ID: entity.NewID(), + } + bMock.EXPECT().GetBook(b.ID).Return(b, nil) + uMock.EXPECT().ListUsers().Return([]*entity.User{u}, nil) + err := uc.Return(b) + assert.Equal(t, entity.ErrBookNotBorrowed, err) + }) + t.Run("success", func(t *testing.T) { + u := &entity.User{ + ID: entity.NewID(), + } + b := &entity.Book{ + ID: entity.NewID(), + } + u.AddBook(b.ID) + bMock.EXPECT().GetBook(b.ID).Return(b, nil) + uMock.EXPECT().GetUser(u.ID).Return(u, nil) + uMock.EXPECT().ListUsers().Return([]*entity.User{u}, nil) + uMock.EXPECT().UpdateUser(u).Return(nil) + bMock.EXPECT().UpdateBook(b).Return(nil) + err := uc.Return(b) + assert.Nil(t, err) + }) +} diff --git a/usecase/user/inmem.go b/usecase/user/inmem.go new file mode 100644 index 0000000..651c6d4 --- /dev/null +++ b/usecase/user/inmem.go @@ -0,0 +1,78 @@ +package user + +import ( + "fmt" + "strings" + + "github.com/eminetto/clean-architecture-go-v2/entity" +) + +//inmem in memory repo +type inmem struct { + m map[entity.ID]*entity.User +} + +//newInmem create new repository +func newInmem() *inmem { + var m = map[entity.ID]*entity.User{} + return &inmem{ + m: m, + } +} + +//Create an user +func (r *inmem) Create(e *entity.User) (entity.ID, error) { + r.m[e.ID] = e + return e.ID, nil +} + +//Get an user +func (r *inmem) Get(id entity.ID) (*entity.User, error) { + if r.m[id] == nil { + return nil, entity.ErrNotFound + } + return r.m[id], nil +} + +//Update an user +func (r *inmem) Update(e *entity.User) error { + _, err := r.Get(e.ID) + if err != nil { + return err + } + r.m[e.ID] = e + return nil +} + +//Search users +func (r *inmem) Search(query string) ([]*entity.User, error) { + var d []*entity.User + for _, j := range r.m { + if strings.Contains(strings.ToLower(j.FirstName), query) { + d = append(d, j) + } + } + if len(d) == 0 { + return nil, entity.ErrNotFound + } + + return d, nil +} + +//List users +func (r *inmem) List() ([]*entity.User, error) { + var d []*entity.User + for _, j := range r.m { + d = append(d, j) + } + return d, nil +} + +//Delete an user +func (r *inmem) Delete(id entity.ID) error { + if r.m[id] == nil { + return fmt.Errorf("not found") + } + r.m[id] = nil + return nil +} diff --git a/usecase/user/interface.go b/usecase/user/interface.go new file mode 100644 index 0000000..340205a --- /dev/null +++ b/usecase/user/interface.go @@ -0,0 +1,35 @@ +package user + +import ( + "github.com/eminetto/clean-architecture-go-v2/entity" +) + +//Reader interface +type Reader interface { + Get(id entity.ID) (*entity.User, error) + Search(query string) ([]*entity.User, error) + List() ([]*entity.User, error) +} + +//Writer user writer +type Writer interface { + Create(e *entity.User) (entity.ID, error) + Update(e *entity.User) error + Delete(id entity.ID) error +} + +//Repository interface +type Repository interface { + Reader + Writer +} + +//UseCase interface +type UseCase interface { + GetUser(id entity.ID) (*entity.User, error) + SearchUsers(query string) ([]*entity.User, error) + ListUsers() ([]*entity.User, error) + CreateUser(email, password, firstName, lastName string) (entity.ID, error) + UpdateUser(e *entity.User) error + DeleteUser(id entity.ID) error +} diff --git a/domain/entity/user/mock/user.go b/usecase/user/mock/user.go similarity index 65% rename from domain/entity/user/mock/user.go rename to usecase/user/mock/user.go index 506b675..3a8e2df 100644 --- a/domain/entity/user/mock/user.go +++ b/usecase/user/mock/user.go @@ -1,12 +1,11 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: domain/entity/user/interface.go +// Source: usecase/user/interface.go // Package mock is a generated GoMock package. package mock import ( - entity "github.com/eminetto/clean-architecture-go-v2/domain/entity" - user "github.com/eminetto/clean-architecture-go-v2/domain/entity/user" + entity "github.com/eminetto/clean-architecture-go-v2/entity" gomock "github.com/golang/mock/gomock" reflect "reflect" ) @@ -35,10 +34,10 @@ func (m *MockReader) EXPECT() *MockReaderMockRecorder { } // Get mocks base method -func (m *MockReader) Get(id entity.ID) (*user.User, error) { +func (m *MockReader) Get(id entity.ID) (*entity.User, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Get", id) - ret0, _ := ret[0].(*user.User) + ret0, _ := ret[0].(*entity.User) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -50,10 +49,10 @@ func (mr *MockReaderMockRecorder) Get(id interface{}) *gomock.Call { } // Search mocks base method -func (m *MockReader) Search(query string) ([]*user.User, error) { +func (m *MockReader) Search(query string) ([]*entity.User, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Search", query) - ret0, _ := ret[0].([]*user.User) + ret0, _ := ret[0].([]*entity.User) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -65,10 +64,10 @@ func (mr *MockReaderMockRecorder) Search(query interface{}) *gomock.Call { } // List mocks base method -func (m *MockReader) List() ([]*user.User, error) { +func (m *MockReader) List() ([]*entity.User, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "List") - ret0, _ := ret[0].([]*user.User) + ret0, _ := ret[0].([]*entity.User) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -103,7 +102,7 @@ func (m *MockWriter) EXPECT() *MockWriterMockRecorder { } // Create mocks base method -func (m *MockWriter) Create(e *user.User) (entity.ID, error) { +func (m *MockWriter) Create(e *entity.User) (entity.ID, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Create", e) ret0, _ := ret[0].(entity.ID) @@ -118,7 +117,7 @@ func (mr *MockWriterMockRecorder) Create(e interface{}) *gomock.Call { } // Update mocks base method -func (m *MockWriter) Update(e *user.User) error { +func (m *MockWriter) Update(e *entity.User) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Update", e) ret0, _ := ret[0].(error) @@ -169,10 +168,10 @@ func (m *MockRepository) EXPECT() *MockRepositoryMockRecorder { } // Get mocks base method -func (m *MockRepository) Get(id entity.ID) (*user.User, error) { +func (m *MockRepository) Get(id entity.ID) (*entity.User, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Get", id) - ret0, _ := ret[0].(*user.User) + ret0, _ := ret[0].(*entity.User) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -184,10 +183,10 @@ func (mr *MockRepositoryMockRecorder) Get(id interface{}) *gomock.Call { } // Search mocks base method -func (m *MockRepository) Search(query string) ([]*user.User, error) { +func (m *MockRepository) Search(query string) ([]*entity.User, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Search", query) - ret0, _ := ret[0].([]*user.User) + ret0, _ := ret[0].([]*entity.User) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -199,10 +198,10 @@ func (mr *MockRepositoryMockRecorder) Search(query interface{}) *gomock.Call { } // List mocks base method -func (m *MockRepository) List() ([]*user.User, error) { +func (m *MockRepository) List() ([]*entity.User, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "List") - ret0, _ := ret[0].([]*user.User) + ret0, _ := ret[0].([]*entity.User) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -214,7 +213,7 @@ func (mr *MockRepositoryMockRecorder) List() *gomock.Call { } // Create mocks base method -func (m *MockRepository) Create(e *user.User) (entity.ID, error) { +func (m *MockRepository) Create(e *entity.User) (entity.ID, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Create", e) ret0, _ := ret[0].(entity.ID) @@ -229,7 +228,7 @@ func (mr *MockRepositoryMockRecorder) Create(e interface{}) *gomock.Call { } // Update mocks base method -func (m *MockRepository) Update(e *user.User) error { +func (m *MockRepository) Update(e *entity.User) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Update", e) ret0, _ := ret[0].(error) @@ -256,113 +255,113 @@ func (mr *MockRepositoryMockRecorder) Delete(id interface{}) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockRepository)(nil).Delete), id) } -// MockManager is a mock of Manager interface -type MockManager struct { +// MockUseCase is a mock of UseCase interface +type MockUseCase struct { ctrl *gomock.Controller - recorder *MockManagerMockRecorder + recorder *MockUseCaseMockRecorder } -// MockManagerMockRecorder is the mock recorder for MockManager -type MockManagerMockRecorder struct { - mock *MockManager +// MockUseCaseMockRecorder is the mock recorder for MockUseCase +type MockUseCaseMockRecorder struct { + mock *MockUseCase } -// NewMockManager creates a new mock instance -func NewMockManager(ctrl *gomock.Controller) *MockManager { - mock := &MockManager{ctrl: ctrl} - mock.recorder = &MockManagerMockRecorder{mock} +// NewMockUseCase creates a new mock instance +func NewMockUseCase(ctrl *gomock.Controller) *MockUseCase { + mock := &MockUseCase{ctrl: ctrl} + mock.recorder = &MockUseCaseMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use -func (m *MockManager) EXPECT() *MockManagerMockRecorder { +func (m *MockUseCase) EXPECT() *MockUseCaseMockRecorder { return m.recorder } -// Get mocks base method -func (m *MockManager) Get(id entity.ID) (*user.User, error) { +// GetUser mocks base method +func (m *MockUseCase) GetUser(id entity.ID) (*entity.User, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Get", id) - ret0, _ := ret[0].(*user.User) + ret := m.ctrl.Call(m, "GetUser", id) + ret0, _ := ret[0].(*entity.User) ret1, _ := ret[1].(error) return ret0, ret1 } -// Get indicates an expected call of Get -func (mr *MockManagerMockRecorder) Get(id interface{}) *gomock.Call { +// GetUser indicates an expected call of GetUser +func (mr *MockUseCaseMockRecorder) GetUser(id interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockManager)(nil).Get), id) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUser", reflect.TypeOf((*MockUseCase)(nil).GetUser), id) } -// Search mocks base method -func (m *MockManager) Search(query string) ([]*user.User, error) { +// SearchUsers mocks base method +func (m *MockUseCase) SearchUsers(query string) ([]*entity.User, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Search", query) - ret0, _ := ret[0].([]*user.User) + ret := m.ctrl.Call(m, "SearchUsers", query) + ret0, _ := ret[0].([]*entity.User) ret1, _ := ret[1].(error) return ret0, ret1 } -// Search indicates an expected call of Search -func (mr *MockManagerMockRecorder) Search(query interface{}) *gomock.Call { +// SearchUsers indicates an expected call of SearchUsers +func (mr *MockUseCaseMockRecorder) SearchUsers(query interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Search", reflect.TypeOf((*MockManager)(nil).Search), query) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchUsers", reflect.TypeOf((*MockUseCase)(nil).SearchUsers), query) } -// List mocks base method -func (m *MockManager) List() ([]*user.User, error) { +// ListUsers mocks base method +func (m *MockUseCase) ListUsers() ([]*entity.User, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "List") - ret0, _ := ret[0].([]*user.User) + ret := m.ctrl.Call(m, "ListUsers") + ret0, _ := ret[0].([]*entity.User) ret1, _ := ret[1].(error) return ret0, ret1 } -// List indicates an expected call of List -func (mr *MockManagerMockRecorder) List() *gomock.Call { +// ListUsers indicates an expected call of ListUsers +func (mr *MockUseCaseMockRecorder) ListUsers() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockManager)(nil).List)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListUsers", reflect.TypeOf((*MockUseCase)(nil).ListUsers)) } -// Create mocks base method -func (m *MockManager) Create(e *user.User) (entity.ID, error) { +// CreateUser mocks base method +func (m *MockUseCase) CreateUser(email, password, firstName, lastName string) (entity.ID, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Create", e) + ret := m.ctrl.Call(m, "CreateUser", email, password, firstName, lastName) ret0, _ := ret[0].(entity.ID) ret1, _ := ret[1].(error) return ret0, ret1 } -// Create indicates an expected call of Create -func (mr *MockManagerMockRecorder) Create(e interface{}) *gomock.Call { +// CreateUser indicates an expected call of CreateUser +func (mr *MockUseCaseMockRecorder) CreateUser(email, password, firstName, lastName interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockManager)(nil).Create), e) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateUser", reflect.TypeOf((*MockUseCase)(nil).CreateUser), email, password, firstName, lastName) } -// Update mocks base method -func (m *MockManager) Update(e *user.User) error { +// UpdateUser mocks base method +func (m *MockUseCase) UpdateUser(e *entity.User) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Update", e) + ret := m.ctrl.Call(m, "UpdateUser", e) ret0, _ := ret[0].(error) return ret0 } -// Update indicates an expected call of Update -func (mr *MockManagerMockRecorder) Update(e interface{}) *gomock.Call { +// UpdateUser indicates an expected call of UpdateUser +func (mr *MockUseCaseMockRecorder) UpdateUser(e interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockManager)(nil).Update), e) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUser", reflect.TypeOf((*MockUseCase)(nil).UpdateUser), e) } -// Delete mocks base method -func (m *MockManager) Delete(id entity.ID) error { +// DeleteUser mocks base method +func (m *MockUseCase) DeleteUser(id entity.ID) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Delete", id) + ret := m.ctrl.Call(m, "DeleteUser", id) ret0, _ := ret[0].(error) return ret0 } -// Delete indicates an expected call of Delete -func (mr *MockManagerMockRecorder) Delete(id interface{}) *gomock.Call { +// DeleteUser indicates an expected call of DeleteUser +func (mr *MockUseCaseMockRecorder) DeleteUser(id interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockManager)(nil).Delete), id) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteUser", reflect.TypeOf((*MockUseCase)(nil).DeleteUser), id) } diff --git a/usecase/user/service.go b/usecase/user/service.go new file mode 100644 index 0000000..2f257b9 --- /dev/null +++ b/usecase/user/service.go @@ -0,0 +1,69 @@ +package user + +import ( + "strings" + "time" + + "github.com/eminetto/clean-architecture-go-v2/entity" +) + +//Service interface +type Service struct { + repo Repository +} + +//NewService create new use case +func NewService(r Repository) *Service { + return &Service{ + repo: r, + } +} + +//CreateUser Create an user +func (s *Service) CreateUser(email, password, firstName, lastName string) (entity.ID, error) { + e, err := entity.NewUser(email, password, firstName, lastName) + if err != nil { + return e.ID, err + } + return s.repo.Create(e) +} + +//GetUser Get an user +func (s *Service) GetUser(id entity.ID) (*entity.User, error) { + return s.repo.Get(id) +} + +//SearchUsers Search users +func (s *Service) SearchUsers(query string) ([]*entity.User, error) { + return s.repo.Search(strings.ToLower(query)) +} + +//ListUsers List users +func (s *Service) ListUsers() ([]*entity.User, error) { + return s.repo.List() +} + +//DeleteUser Delete an user +func (s *Service) DeleteUser(id entity.ID) error { + u, err := s.GetUser(id) + if u == nil { + return entity.ErrNotFound + } + if err != nil { + return err + } + if len(u.Books) > 0 { + return entity.ErrCannotBeDeleted + } + return s.repo.Delete(id) +} + +//UpdateUser Update an user +func (s *Service) UpdateUser(e *entity.User) error { + err := e.Validate() + if err != nil { + return entity.ErrInvalidEntity + } + e.UpdatedAt = time.Now() + return s.repo.Update(e) +} diff --git a/usecase/user/service_test.go b/usecase/user/service_test.go new file mode 100644 index 0000000..99fafc4 --- /dev/null +++ b/usecase/user/service_test.go @@ -0,0 +1,105 @@ +package user + +import ( + "testing" + "time" + + "github.com/eminetto/clean-architecture-go-v2/entity" + + "github.com/stretchr/testify/assert" +) + +func newFixtureUser() *entity.User { + return &entity.User{ + ID: entity.NewID(), + Email: "ozzy@metalgods.net", + Password: "123456", + FirstName: "Ozzy", + LastName: "Osbourne", + CreatedAt: time.Now(), + } +} + +func Test_Create(t *testing.T) { + repo := newInmem() + m := NewService(repo) + u := newFixtureUser() + _, err := m.CreateUser(u.Email, u.Password, u.FirstName, u.LastName) + assert.Nil(t, err) + assert.False(t, u.CreatedAt.IsZero()) + assert.True(t, u.UpdatedAt.IsZero()) +} + +func Test_SearchAndFind(t *testing.T) { + repo := newInmem() + m := NewService(repo) + u1 := newFixtureUser() + u2 := newFixtureUser() + u2.FirstName = "Lemmy" + + uID, _ := m.CreateUser(u1.Email, u1.Password, u1.FirstName, u1.LastName) + _, _ = m.CreateUser(u2.Email, u2.Password, u2.FirstName, u2.LastName) + + t.Run("search", func(t *testing.T) { + c, err := m.SearchUsers("ozzy") + assert.Nil(t, err) + assert.Equal(t, 1, len(c)) + assert.Equal(t, "Osbourne", c[0].LastName) + + c, err = m.SearchUsers("dio") + assert.Equal(t, entity.ErrNotFound, err) + assert.Nil(t, c) + }) + t.Run("list all", func(t *testing.T) { + all, err := m.ListUsers() + assert.Nil(t, err) + assert.Equal(t, 2, len(all)) + }) + + t.Run("get", func(t *testing.T) { + saved, err := m.GetUser(uID) + assert.Nil(t, err) + assert.Equal(t, u1.FirstName, saved.FirstName) + }) +} + +func Test_Update(t *testing.T) { + repo := newInmem() + m := NewService(repo) + u := newFixtureUser() + id, err := m.CreateUser(u.Email, u.Password, u.FirstName, u.LastName) + assert.Nil(t, err) + saved, _ := m.GetUser(id) + saved.FirstName = "Dio" + saved.Books = append(saved.Books, entity.NewID()) + assert.Nil(t, m.UpdateUser(saved)) + updated, err := m.GetUser(id) + assert.Nil(t, err) + assert.Equal(t, "Dio", updated.FirstName) + assert.False(t, updated.UpdatedAt.IsZero()) + assert.Equal(t, 1, len(updated.Books)) +} + +func TestDelete(t *testing.T) { + repo := newInmem() + m := NewService(repo) + u1 := newFixtureUser() + u2 := newFixtureUser() + u2ID, _ := m.CreateUser(u2.Email, u2.Password, u2.FirstName, u2.LastName) + + err := m.DeleteUser(u1.ID) + assert.Equal(t, entity.ErrNotFound, err) + + err = m.DeleteUser(u2ID) + assert.Nil(t, err) + _, err = m.GetUser(u2ID) + assert.Equal(t, entity.ErrNotFound, err) + + u3 := newFixtureUser() + id, _ := m.CreateUser(u3.Email, u3.Password, u3.FirstName, u3.LastName) + saved, _ := m.GetUser(id) + saved.Books = []entity.ID{entity.NewID()} + _ = m.UpdateUser(saved) + err = m.DeleteUser(id) + assert.Equal(t, entity.ErrCannotBeDeleted, err) +}