Merge pull request #4 from eminetto/issue-3

feat: improvements in structures
This commit is contained in:
Elton Minetto
2020-10-16 16:29:00 -03:00
committed by GitHub
54 changed files with 1510 additions and 1201 deletions
+1
View File
@@ -1 +1,2 @@
bin/ bin/
.idea/
+3 -3
View File
@@ -32,9 +32,9 @@ ci: dependencies test
build-mocks: build-mocks:
@go get github.com/golang/mock/gomock @go get github.com/golang/mock/gomock
@go install github.com/golang/mock/mockgen @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=usecase/book/interface.go -destination=usecase/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=usecase/user/interface.go -destination=usecase/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/loan/interface.go -destination=usecase/loan/mock/loan.go -package=mock
test: test:
go test -tags testing ./... go test -tags testing ./...
+24 -36
View File
@@ -4,33 +4,30 @@ import (
"encoding/json" "encoding/json"
"log" "log"
"net/http" "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/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/codegangsta/negroni"
"github.com/eminetto/clean-architecture-go-v2/entity"
"github.com/gorilla/mux" "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) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
errorMessage := "Error reading books" errorMessage := "Error reading books"
var data []*book.Book var data []*entity.Book
var err error var err error
title := r.URL.Query().Get("title") title := r.URL.Query().Get("title")
switch { switch {
case title == "": case title == "":
data, err = manager.List() data, err = service.ListBooks()
default: default:
data, err = manager.Search(title) data, err = service.SearchBooks(title)
} }
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
if err != nil && err != domain.ErrNotFound { if err != nil && err != entity.ErrNotFound {
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(errorMessage)) w.Write([]byte(errorMessage))
return 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) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
errorMessage := "Error adding book" errorMessage := "Error adding book"
var input struct { var input struct {
@@ -74,27 +71,18 @@ func createBook(manager book.Manager) http.Handler {
w.Write([]byte(errorMessage)) w.Write([]byte(errorMessage))
return return
} }
b := &book.Book{ id, err := service.CreateBook(input.Title, input.Author, input.Pages, input.Quantity)
ID: entity.NewID(),
Title: input.Title,
Author: input.Author,
Pages: input.Pages,
Quantity: input.Quantity,
CreatedAt: time.Now(),
}
b.ID, err = manager.Create(b)
if err != nil { if err != nil {
log.Println(err.Error())
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(errorMessage)) w.Write([]byte(errorMessage))
return return
} }
toJ := &presenter.Book{ toJ := &presenter.Book{
ID: b.ID, ID: id,
Title: b.Title, Title: input.Title,
Author: b.Author, Author: input.Author,
Pages: b.Pages, Pages: input.Pages,
Quantity: b.Quantity, Quantity: input.Quantity,
} }
w.WriteHeader(http.StatusCreated) 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) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
errorMessage := "Error reading book" errorMessage := "Error reading book"
vars := mux.Vars(r) vars := mux.Vars(r)
@@ -117,8 +105,8 @@ func getBook(manager book.Manager) http.Handler {
w.Write([]byte(errorMessage)) w.Write([]byte(errorMessage))
return return
} }
data, err := manager.Get(id) data, err := service.GetBook(id)
if err != nil && err != domain.ErrNotFound { if err != nil && err != entity.ErrNotFound {
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(errorMessage)) w.Write([]byte(errorMessage))
return 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) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
errorMessage := "Error removing bookmark" errorMessage := "Error removing bookmark"
vars := mux.Vars(r) vars := mux.Vars(r)
@@ -153,7 +141,7 @@ func deleteBook(manager book.Manager) http.Handler {
w.Write([]byte(errorMessage)) w.Write([]byte(errorMessage))
return return
} }
err = manager.Delete(id) err = service.DeleteBook(id)
if err != nil { if err != nil {
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(errorMessage)) w.Write([]byte(errorMessage))
@@ -163,20 +151,20 @@ func deleteBook(manager book.Manager) http.Handler {
} }
//MakeBookHandlers make url handlers //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( r.Handle("/v1/book", n.With(
negroni.Wrap(listBooks(manager)), negroni.Wrap(listBooks(service)),
)).Methods("GET", "OPTIONS").Name("listBooks") )).Methods("GET", "OPTIONS").Name("listBooks")
r.Handle("/v1/book", n.With( r.Handle("/v1/book", n.With(
negroni.Wrap(createBook(manager)), negroni.Wrap(createBook(service)),
)).Methods("POST", "OPTIONS").Name("createBook") )).Methods("POST", "OPTIONS").Name("createBook")
r.Handle("/v1/book/{id}", n.With( r.Handle("/v1/book/{id}", n.With(
negroni.Wrap(getBook(manager)), negroni.Wrap(getBook(service)),
)).Methods("GET", "OPTIONS").Name("getBook") )).Methods("GET", "OPTIONS").Name("getBook")
r.Handle("/v1/book/{id}", n.With( r.Handle("/v1/book/{id}", n.With(
negroni.Wrap(deleteBook(manager)), negroni.Wrap(deleteBook(service)),
)).Methods("DELETE", "OPTIONS").Name("deleteBook") )).Methods("DELETE", "OPTIONS").Name("deleteBook")
} }
+46 -41
View File
@@ -8,13 +8,10 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/eminetto/clean-architecture-go-v2/domain" "github.com/eminetto/clean-architecture-go-v2/entity"
"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/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/golang/mock/gomock"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@@ -23,18 +20,20 @@ import (
func Test_listBooks(t *testing.T) { func Test_listBooks(t *testing.T) {
controller := gomock.NewController(t) controller := gomock.NewController(t)
defer controller.Finish() defer controller.Finish()
manager := mock.NewMockManager(controller) service := mock.NewMockUseCase(controller)
r := mux.NewRouter() r := mux.NewRouter()
n := negroni.New() n := negroni.New()
MakeBookHandlers(r, *n, manager) MakeBookHandlers(r, *n, service)
path, err := r.GetRoute("listBooks").GetPathTemplate() path, err := r.GetRoute("listBooks").GetPathTemplate()
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, "/v1/book", path) assert.Equal(t, "/v1/book", path)
b := book.NewFixtureBook() b := &entity.Book{
manager.EXPECT(). ID: entity.NewID(),
List(). }
Return([]*book.Book{b}, nil) service.EXPECT().
ts := httptest.NewServer(listBooks(manager)) ListBooks().
Return([]*entity.Book{b}, nil)
ts := httptest.NewServer(listBooks(service))
defer ts.Close() defer ts.Close()
res, err := http.Get(ts.URL) res, err := http.Get(ts.URL)
assert.Nil(t, err) assert.Nil(t, err)
@@ -44,12 +43,12 @@ func Test_listBooks(t *testing.T) {
func Test_listBooks_NotFound(t *testing.T) { func Test_listBooks_NotFound(t *testing.T) {
controller := gomock.NewController(t) controller := gomock.NewController(t)
defer controller.Finish() defer controller.Finish()
manager := mock.NewMockManager(controller) service := mock.NewMockUseCase(controller)
ts := httptest.NewServer(listBooks(manager)) ts := httptest.NewServer(listBooks(service))
defer ts.Close() defer ts.Close()
manager.EXPECT(). service.EXPECT().
Search("book of books"). SearchBooks("book of books").
Return(nil, domain.ErrNotFound) Return(nil, entity.ErrNotFound)
res, err := http.Get(ts.URL + "?title=book+of+books") res, err := http.Get(ts.URL + "?title=book+of+books")
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, http.StatusNotFound, res.StatusCode) 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) { func Test_listBooks_Search(t *testing.T) {
controller := gomock.NewController(t) controller := gomock.NewController(t)
defer controller.Finish() defer controller.Finish()
manager := mock.NewMockManager(controller) service := mock.NewMockUseCase(controller)
b := book.NewFixtureBook() b := &entity.Book{
manager.EXPECT(). ID: entity.NewID(),
Search("ozzy"). }
Return([]*book.Book{b}, nil) service.EXPECT().
ts := httptest.NewServer(listBooks(manager)) SearchBooks("ozzy").
Return([]*entity.Book{b}, nil)
ts := httptest.NewServer(listBooks(service))
defer ts.Close() defer ts.Close()
res, err := http.Get(ts.URL + "?title=ozzy") res, err := http.Get(ts.URL + "?title=ozzy")
assert.Nil(t, err) assert.Nil(t, err)
@@ -73,18 +74,18 @@ func Test_listBooks_Search(t *testing.T) {
func Test_createBook(t *testing.T) { func Test_createBook(t *testing.T) {
controller := gomock.NewController(t) controller := gomock.NewController(t)
defer controller.Finish() defer controller.Finish()
manager := mock.NewMockManager(controller) service := mock.NewMockUseCase(controller)
r := mux.NewRouter() r := mux.NewRouter()
n := negroni.New() n := negroni.New()
MakeBookHandlers(r, *n, manager) MakeBookHandlers(r, *n, service)
path, err := r.GetRoute("createBook").GetPathTemplate() path, err := r.GetRoute("createBook").GetPathTemplate()
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, "/v1/book", path) assert.Equal(t, "/v1/book", path)
manager.EXPECT(). service.EXPECT().
Create(gomock.Any()). CreateBook(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(entity.NewID(), nil) Return(entity.NewID(), nil)
h := createBook(manager) h := createBook(service)
ts := httptest.NewServer(h) ts := httptest.NewServer(h)
defer ts.Close() 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)) resp, _ := http.Post(ts.URL+"/v1/book", "application/json", strings.NewReader(payload))
assert.Equal(t, http.StatusCreated, resp.StatusCode) assert.Equal(t, http.StatusCreated, resp.StatusCode)
var b *book.Book var b *entity.Book
json.NewDecoder(resp.Body).Decode(&b) json.NewDecoder(resp.Body).Decode(&b)
assert.Equal(t, "Ozzy Osbourne", b.Author) assert.Equal(t, "Ozzy Osbourne", b.Author)
} }
@@ -105,25 +106,27 @@ func Test_createBook(t *testing.T) {
func Test_getBook(t *testing.T) { func Test_getBook(t *testing.T) {
controller := gomock.NewController(t) controller := gomock.NewController(t)
defer controller.Finish() defer controller.Finish()
manager := mock.NewMockManager(controller) service := mock.NewMockUseCase(controller)
r := mux.NewRouter() r := mux.NewRouter()
n := negroni.New() n := negroni.New()
MakeBookHandlers(r, *n, manager) MakeBookHandlers(r, *n, service)
path, err := r.GetRoute("getBook").GetPathTemplate() path, err := r.GetRoute("getBook").GetPathTemplate()
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, "/v1/book/{id}", path) assert.Equal(t, "/v1/book/{id}", path)
b := book.NewFixtureBook() b := &entity.Book{
manager.EXPECT(). ID: entity.NewID(),
Get(b.ID). }
service.EXPECT().
GetBook(b.ID).
Return(b, nil) Return(b, nil)
handler := getBook(manager) handler := getBook(service)
r.Handle("/v1/book/{id}", handler) r.Handle("/v1/book/{id}", handler)
ts := httptest.NewServer(r) ts := httptest.NewServer(r)
defer ts.Close() defer ts.Close()
res, err := http.Get(ts.URL + "/v1/book/" + b.ID.String()) res, err := http.Get(ts.URL + "/v1/book/" + b.ID.String())
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, http.StatusOK, res.StatusCode) assert.Equal(t, http.StatusOK, res.StatusCode)
var d *book.Book var d *entity.Book
json.NewDecoder(res.Body).Decode(&d) json.NewDecoder(res.Body).Decode(&d)
assert.NotNil(t, d) assert.NotNil(t, d)
assert.Equal(t, b.ID, d.ID) assert.Equal(t, b.ID, d.ID)
@@ -132,16 +135,18 @@ func Test_getBook(t *testing.T) {
func Test_deleteBook(t *testing.T) { func Test_deleteBook(t *testing.T) {
controller := gomock.NewController(t) controller := gomock.NewController(t)
defer controller.Finish() defer controller.Finish()
manager := mock.NewMockManager(controller) service := mock.NewMockUseCase(controller)
r := mux.NewRouter() r := mux.NewRouter()
n := negroni.New() n := negroni.New()
MakeBookHandlers(r, *n, manager) MakeBookHandlers(r, *n, service)
path, err := r.GetRoute("deleteBook").GetPathTemplate() path, err := r.GetRoute("deleteBook").GetPathTemplate()
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, "/v1/book/{id}", path) assert.Equal(t, "/v1/book/{id}", path)
b := book.NewFixtureBook() b := &entity.Book{
manager.EXPECT().Delete(b.ID).Return(nil) ID: entity.NewID(),
handler := deleteBook(manager) }
service.EXPECT().DeleteBook(b.ID).Return(nil)
handler := deleteBook(service)
req, _ := http.NewRequest("DELETE", "/v1/bookmark/"+b.ID.String(), nil) req, _ := http.NewRequest("DELETE", "/v1/bookmark/"+b.ID.String(), nil)
r.Handle("/v1/bookmark/{id}", handler).Methods("DELETE", "OPTIONS") r.Handle("/v1/bookmark/{id}", handler).Methods("DELETE", "OPTIONS")
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
+18 -21
View File
@@ -4,20 +4,17 @@ import (
"fmt" "fmt"
"net/http" "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/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/codegangsta/negroni" "github.com/codegangsta/negroni"
"github.com/eminetto/clean-architecture-go-v2/entity"
"github.com/gorilla/mux" "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) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
errorMessage := "Error borrowing book" errorMessage := "Error borrowing book"
vars := mux.Vars(r) vars := mux.Vars(r)
@@ -27,8 +24,8 @@ func borrowBook(bManager book.Manager, uManager user.Manager, loanUseCase loan.U
w.Write([]byte(errorMessage)) w.Write([]byte(errorMessage))
return return
} }
b, err := bManager.Get(bID) b, err := bookService.GetBook(bID)
if err != nil && err != domain.ErrNotFound { if err != nil && err != entity.ErrNotFound {
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(errorMessage)) w.Write([]byte(errorMessage))
return return
@@ -44,8 +41,8 @@ func borrowBook(bManager book.Manager, uManager user.Manager, loanUseCase loan.U
w.Write([]byte(errorMessage)) w.Write([]byte(errorMessage))
return return
} }
u, err := uManager.Get(uID) u, err := userService.GetUser(uID)
if err != nil && err != domain.ErrNotFound { if err != nil && err != entity.ErrNotFound {
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(errorMessage)) w.Write([]byte(errorMessage))
return return
@@ -55,7 +52,7 @@ func borrowBook(bManager book.Manager, uManager user.Manager, loanUseCase loan.U
w.Write([]byte(errorMessage)) w.Write([]byte(errorMessage))
return return
} }
err = loanUseCase.Borrow(u, b) err = loanService.Borrow(u, b)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
w.WriteHeader(http.StatusInternalServerError) 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) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
errorMessage := "Error returning book" errorMessage := "Error returning book"
vars := mux.Vars(r) vars := mux.Vars(r)
@@ -76,8 +73,8 @@ func returnBook(bManager book.Manager, loanUseCase loan.UseCase) http.Handler {
w.Write([]byte(errorMessage)) w.Write([]byte(errorMessage))
return return
} }
b, err := bManager.Get(bID) b, err := bookService.GetBook(bID)
if err != nil && err != domain.ErrNotFound { if err != nil && err != entity.ErrNotFound {
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(errorMessage)) w.Write([]byte(errorMessage))
return return
@@ -87,8 +84,8 @@ func returnBook(bManager book.Manager, loanUseCase loan.UseCase) http.Handler {
w.Write([]byte(errorMessage)) w.Write([]byte(errorMessage))
return return
} }
err = loanUseCase.Return(b) err = loanService.Return(b)
if err != nil && err != domain.ErrNotFound { if err != nil && err != entity.ErrNotFound {
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(errorMessage)) w.Write([]byte(errorMessage))
return return
@@ -98,12 +95,12 @@ func returnBook(bManager book.Manager, loanUseCase loan.UseCase) http.Handler {
} }
//MakeLoanHandlers make url handlers //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( 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") )).Methods("GET", "OPTIONS").Name("borrowBook")
r.Handle("/v1/loan/return/{book_id}", n.With( r.Handle("/v1/loan/return/{book_id}", n.With(
negroni.Wrap(returnBook(bManager, loanUseCase)), negroni.Wrap(returnBook(bookService, loanService)),
)).Methods("GET", "OPTIONS").Name("returnBook") )).Methods("GET", "OPTIONS").Name("returnBook")
} }
+27 -24
View File
@@ -6,17 +6,12 @@ import (
"net/http/httptest" "net/http/httptest"
"testing" "testing"
"github.com/eminetto/clean-architecture-go-v2/domain/entity/user" "github.com/eminetto/clean-architecture-go-v2/entity"
"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/codegangsta/negroni" "github.com/codegangsta/negroni"
bmock "github.com/eminetto/clean-architecture-go-v2/domain/entity/book/mock" bmock "github.com/eminetto/clean-architecture-go-v2/usecase/book/mock"
umock "github.com/eminetto/clean-architecture-go-v2/domain/entity/user/mock" lmock "github.com/eminetto/clean-architecture-go-v2/usecase/loan/mock"
lmock "github.com/eminetto/clean-architecture-go-v2/domain/usecase/loan/mock" umock "github.com/eminetto/clean-architecture-go-v2/usecase/user/mock"
"github.com/golang/mock/gomock" "github.com/golang/mock/gomock"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@@ -25,8 +20,8 @@ import (
func Test_borrowBook(t *testing.T) { func Test_borrowBook(t *testing.T) {
controller := gomock.NewController(t) controller := gomock.NewController(t)
defer controller.Finish() defer controller.Finish()
uMock := umock.NewMockManager(controller) uMock := umock.NewMockUseCase(controller)
bMock := bmock.NewMockManager(controller) bMock := bmock.NewMockUseCase(controller)
lMock := lmock.NewMockUseCase(controller) lMock := lmock.NewMockUseCase(controller)
r := mux.NewRouter() r := mux.NewRouter()
n := negroni.New() n := negroni.New()
@@ -39,7 +34,7 @@ func Test_borrowBook(t *testing.T) {
t.Run("book not found", func(t *testing.T) { t.Run("book not found", func(t *testing.T) {
bID := entity.NewID() bID := entity.NewID()
uID := 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) ts := httptest.NewServer(r)
defer ts.Close() defer ts.Close()
res, err := http.Get(fmt.Sprintf("%s/v1/loan/borrow/%s/%s", ts.URL, bID.String(), uID.String())) 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) assert.Equal(t, http.StatusNotFound, res.StatusCode)
}) })
t.Run("user not found", func(t *testing.T) { t.Run("user not found", func(t *testing.T) {
b := book.NewFixtureBook() b := &entity.Book{
ID: entity.NewID(),
}
uID := entity.NewID() uID := entity.NewID()
bMock.EXPECT().Get(b.ID).Return(b, nil) bMock.EXPECT().GetBook(b.ID).Return(b, nil)
uMock.EXPECT().Get(uID).Return(nil, domain.ErrNotFound) uMock.EXPECT().GetUser(uID).Return(nil, entity.ErrNotFound)
ts := httptest.NewServer(r) ts := httptest.NewServer(r)
defer ts.Close() defer ts.Close()
res, err := http.Get(fmt.Sprintf("%s/v1/loan/borrow/%s/%s", ts.URL, b.ID.String(), uID.String())) 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) assert.Equal(t, http.StatusNotFound, res.StatusCode)
}) })
t.Run("success", func(t *testing.T) { t.Run("success", func(t *testing.T) {
b := book.NewFixtureBook() b := &entity.Book{
u := user.NewFixtureUser() ID: entity.NewID(),
bMock.EXPECT().Get(b.ID).Return(b, nil) }
uMock.EXPECT().Get(u.ID).Return(u, nil) 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) lMock.EXPECT().Borrow(u, b).Return(nil)
ts := httptest.NewServer(r) ts := httptest.NewServer(r)
defer ts.Close() defer ts.Close()
@@ -74,8 +75,8 @@ func Test_borrowBook(t *testing.T) {
func Test_returnBook(t *testing.T) { func Test_returnBook(t *testing.T) {
controller := gomock.NewController(t) controller := gomock.NewController(t)
defer controller.Finish() defer controller.Finish()
uMock := umock.NewMockManager(controller) uMock := umock.NewMockUseCase(controller)
bMock := bmock.NewMockManager(controller) bMock := bmock.NewMockUseCase(controller)
lMock := lmock.NewMockUseCase(controller) lMock := lmock.NewMockUseCase(controller)
r := mux.NewRouter() r := mux.NewRouter()
n := negroni.New() n := negroni.New()
@@ -87,7 +88,7 @@ func Test_returnBook(t *testing.T) {
r.Handle("/v1/loan/return/{book_id}", handler) r.Handle("/v1/loan/return/{book_id}", handler)
t.Run("book not found", func(t *testing.T) { t.Run("book not found", func(t *testing.T) {
bID := entity.NewID() bID := entity.NewID()
bMock.EXPECT().Get(bID).Return(nil, domain.ErrNotFound) bMock.EXPECT().GetBook(bID).Return(nil, entity.ErrNotFound)
ts := httptest.NewServer(r) ts := httptest.NewServer(r)
defer ts.Close() defer ts.Close()
res, err := http.Get(fmt.Sprintf("%s/v1/loan/return/%s", ts.URL, bID.String())) 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) assert.Equal(t, http.StatusNotFound, res.StatusCode)
}) })
t.Run("success", func(t *testing.T) { t.Run("success", func(t *testing.T) {
b := book.NewFixtureBook() b := &entity.Book{
bMock.EXPECT().Get(b.ID).Return(b, nil) ID: entity.NewID(),
}
bMock.EXPECT().GetBook(b.ID).Return(b, nil)
lMock.EXPECT().Return(b).Return(nil) lMock.EXPECT().Return(b).Return(nil)
ts := httptest.NewServer(r) ts := httptest.NewServer(r)
defer ts.Close() defer ts.Close()
+23 -36
View File
@@ -4,34 +4,31 @@ import (
"encoding/json" "encoding/json"
"log" "log"
"net/http" "net/http"
"time"
"github.com/eminetto/clean-architecture-go-v2/domain/entity/user" "github.com/eminetto/clean-architecture-go-v2/usecase/user"
"github.com/eminetto/clean-architecture-go-v2/domain"
"github.com/eminetto/clean-architecture-go-v2/api/presenter" "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/codegangsta/negroni"
"github.com/gorilla/mux" "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) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
errorMessage := "Error reading users" errorMessage := "Error reading users"
var data []*user.User var data []*entity.User
var err error var err error
name := r.URL.Query().Get("name") name := r.URL.Query().Get("name")
switch { switch {
case name == "": case name == "":
data, err = manager.List() data, err = service.ListUsers()
default: default:
data, err = manager.Search(name) data, err = service.SearchUsers(name)
} }
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
if err != nil && err != domain.ErrNotFound { if err != nil && err != entity.ErrNotFound {
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(errorMessage)) w.Write([]byte(errorMessage))
return 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) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
errorMessage := "Error adding user" errorMessage := "Error adding user"
var input struct { var input struct {
@@ -74,16 +71,7 @@ func createUser(manager user.Manager) http.Handler {
w.Write([]byte(errorMessage)) w.Write([]byte(errorMessage))
return return
} }
//TODO: validate data ;) id, err := service.CreateUser(input.Email, input.Password, input.FirstName, input.LastName)
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)
if err != nil { if err != nil {
log.Println(err.Error()) log.Println(err.Error())
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
@@ -91,15 +79,14 @@ func createUser(manager user.Manager) http.Handler {
return return
} }
toJ := &presenter.User{ toJ := &presenter.User{
ID: u.ID, ID: id,
Email: u.Email, Email: input.Email,
FirstName: u.FirstName, FirstName: input.FirstName,
LastName: u.LastName, LastName: input.LastName,
} }
w.WriteHeader(http.StatusCreated) w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(toJ); err != nil { if err := json.NewEncoder(w).Encode(toJ); err != nil {
log.Println(err.Error())
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(errorMessage)) w.Write([]byte(errorMessage))
return 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) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
errorMessage := "Error reading user" errorMessage := "Error reading user"
vars := mux.Vars(r) vars := mux.Vars(r)
@@ -117,9 +104,9 @@ func getUser(manager user.Manager) http.Handler {
w.Write([]byte(errorMessage)) w.Write([]byte(errorMessage))
return return
} }
data, err := manager.Get(id) data, err := service.GetUser(id)
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
if err != nil && err != domain.ErrNotFound { if err != nil && err != entity.ErrNotFound {
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(errorMessage)) w.Write([]byte(errorMessage))
return 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) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
errorMessage := "Error removing user" errorMessage := "Error removing user"
vars := mux.Vars(r) vars := mux.Vars(r)
@@ -153,7 +140,7 @@ func deleteUser(manager user.Manager) http.Handler {
w.Write([]byte(errorMessage)) w.Write([]byte(errorMessage))
return return
} }
err = manager.Delete(id) err = service.DeleteUser(id)
if err != nil { if err != nil {
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(errorMessage)) w.Write([]byte(errorMessage))
@@ -163,20 +150,20 @@ func deleteUser(manager user.Manager) http.Handler {
} }
//MakeUserHandlers make url handlers //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( r.Handle("/v1/user", n.With(
negroni.Wrap(listUsers(manager)), negroni.Wrap(listUsers(service)),
)).Methods("GET", "OPTIONS").Name("listUsers") )).Methods("GET", "OPTIONS").Name("listUsers")
r.Handle("/v1/user", n.With( r.Handle("/v1/user", n.With(
negroni.Wrap(createUser(manager)), negroni.Wrap(createUser(service)),
)).Methods("POST", "OPTIONS").Name("createUser") )).Methods("POST", "OPTIONS").Name("createUser")
r.Handle("/v1/user/{id}", n.With( r.Handle("/v1/user/{id}", n.With(
negroni.Wrap(getUser(manager)), negroni.Wrap(getUser(service)),
)).Methods("GET", "OPTIONS").Name("getUser") )).Methods("GET", "OPTIONS").Name("getUser")
r.Handle("/v1/user/{id}", n.With( r.Handle("/v1/user/{id}", n.With(
negroni.Wrap(deleteUser(manager)), negroni.Wrap(deleteUser(service)),
)).Methods("DELETE", "OPTIONS").Name("deleteUser") )).Methods("DELETE", "OPTIONS").Name("deleteUser")
} }
+29 -23
View File
@@ -10,10 +10,8 @@ import (
"github.com/codegangsta/negroni" "github.com/codegangsta/negroni"
"github.com/eminetto/clean-architecture-go-v2/api/presenter" "github.com/eminetto/clean-architecture-go-v2/api/presenter"
"github.com/eminetto/clean-architecture-go-v2/domain" "github.com/eminetto/clean-architecture-go-v2/entity"
"github.com/eminetto/clean-architecture-go-v2/domain/entity" "github.com/eminetto/clean-architecture-go-v2/usecase/user/mock"
"github.com/eminetto/clean-architecture-go-v2/domain/entity/user"
"github.com/eminetto/clean-architecture-go-v2/domain/entity/user/mock"
"github.com/golang/mock/gomock" "github.com/golang/mock/gomock"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@@ -22,17 +20,19 @@ import (
func Test_listUsers(t *testing.T) { func Test_listUsers(t *testing.T) {
controller := gomock.NewController(t) controller := gomock.NewController(t)
defer controller.Finish() defer controller.Finish()
m := mock.NewMockManager(controller) m := mock.NewMockUseCase(controller)
r := mux.NewRouter() r := mux.NewRouter()
n := negroni.New() n := negroni.New()
MakeUserHandlers(r, *n, m) MakeUserHandlers(r, *n, m)
path, err := r.GetRoute("listUsers").GetPathTemplate() path, err := r.GetRoute("listUsers").GetPathTemplate()
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, "/v1/user", path) assert.Equal(t, "/v1/user", path)
u := user.NewFixtureUser() u := &entity.User{
ID: entity.NewID(),
}
m.EXPECT(). m.EXPECT().
List(). ListUsers().
Return([]*user.User{u}, nil) Return([]*entity.User{u}, nil)
ts := httptest.NewServer(listUsers(m)) ts := httptest.NewServer(listUsers(m))
defer ts.Close() defer ts.Close()
res, err := http.Get(ts.URL) res, err := http.Get(ts.URL)
@@ -43,12 +43,12 @@ func Test_listUsers(t *testing.T) {
func Test_listUsers_NotFound(t *testing.T) { func Test_listUsers_NotFound(t *testing.T) {
controller := gomock.NewController(t) controller := gomock.NewController(t)
defer controller.Finish() defer controller.Finish()
m := mock.NewMockManager(controller) m := mock.NewMockUseCase(controller)
ts := httptest.NewServer(listUsers(m)) ts := httptest.NewServer(listUsers(m))
defer ts.Close() defer ts.Close()
m.EXPECT(). m.EXPECT().
Search("dio"). SearchUsers("dio").
Return(nil, domain.ErrNotFound) Return(nil, entity.ErrNotFound)
res, err := http.Get(ts.URL + "?name=dio") res, err := http.Get(ts.URL + "?name=dio")
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, http.StatusNotFound, res.StatusCode) 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) { func Test_listUsers_Search(t *testing.T) {
controller := gomock.NewController(t) controller := gomock.NewController(t)
defer controller.Finish() defer controller.Finish()
m := mock.NewMockManager(controller) m := mock.NewMockUseCase(controller)
u := user.NewFixtureUser() u := &entity.User{
ID: entity.NewID(),
}
m.EXPECT(). m.EXPECT().
Search("ozzy"). SearchUsers("ozzy").
Return([]*user.User{u}, nil) Return([]*entity.User{u}, nil)
ts := httptest.NewServer(listUsers(m)) ts := httptest.NewServer(listUsers(m))
defer ts.Close() defer ts.Close()
res, err := http.Get(ts.URL + "?name=ozzy") 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) { func Test_createUser(t *testing.T) {
controller := gomock.NewController(t) controller := gomock.NewController(t)
defer controller.Finish() defer controller.Finish()
m := mock.NewMockManager(controller) m := mock.NewMockUseCase(controller)
r := mux.NewRouter() r := mux.NewRouter()
n := negroni.New() n := negroni.New()
MakeUserHandlers(r, *n, m) MakeUserHandlers(r, *n, m)
@@ -81,7 +83,7 @@ func Test_createUser(t *testing.T) {
assert.Equal(t, "/v1/user", path) assert.Equal(t, "/v1/user", path)
m.EXPECT(). m.EXPECT().
Create(gomock.Any()). CreateUser(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(entity.NewID(), nil) Return(entity.NewID(), nil)
h := createUser(m) h := createUser(m)
@@ -105,16 +107,18 @@ func Test_createUser(t *testing.T) {
func Test_getUser(t *testing.T) { func Test_getUser(t *testing.T) {
controller := gomock.NewController(t) controller := gomock.NewController(t)
defer controller.Finish() defer controller.Finish()
m := mock.NewMockManager(controller) m := mock.NewMockUseCase(controller)
r := mux.NewRouter() r := mux.NewRouter()
n := negroni.New() n := negroni.New()
MakeUserHandlers(r, *n, m) MakeUserHandlers(r, *n, m)
path, err := r.GetRoute("getUser").GetPathTemplate() path, err := r.GetRoute("getUser").GetPathTemplate()
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, "/v1/user/{id}", path) assert.Equal(t, "/v1/user/{id}", path)
u := user.NewFixtureUser() u := &entity.User{
ID: entity.NewID(),
}
m.EXPECT(). m.EXPECT().
Get(u.ID). GetUser(u.ID).
Return(u, nil) Return(u, nil)
handler := getUser(m) handler := getUser(m)
r.Handle("/v1/user/{id}", handler) r.Handle("/v1/user/{id}", handler)
@@ -132,15 +136,17 @@ func Test_getUser(t *testing.T) {
func Test_deleteUser(t *testing.T) { func Test_deleteUser(t *testing.T) {
controller := gomock.NewController(t) controller := gomock.NewController(t)
defer controller.Finish() defer controller.Finish()
m := mock.NewMockManager(controller) m := mock.NewMockUseCase(controller)
r := mux.NewRouter() r := mux.NewRouter()
n := negroni.New() n := negroni.New()
MakeUserHandlers(r, *n, m) MakeUserHandlers(r, *n, m)
path, err := r.GetRoute("deleteUser").GetPathTemplate() path, err := r.GetRoute("deleteUser").GetPathTemplate()
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, "/v1/user/{id}", path) assert.Equal(t, "/v1/user/{id}", path)
u := user.NewFixtureUser() u := &entity.User{
m.EXPECT().Delete(u.ID).Return(nil) ID: entity.NewID(),
}
m.EXPECT().DeleteUser(u.ID).Return(nil)
handler := deleteUser(m) handler := deleteUser(m)
req, _ := http.NewRequest("DELETE", "/v1/user/"+u.ID.String(), nil) req, _ := http.NewRequest("DELETE", "/v1/user/"+u.ID.String(), nil)
r.Handle("/v1/user/{id}", handler).Methods("DELETE", "OPTIONS") r.Handle("/v1/user/{id}", handler).Methods("DELETE", "OPTIONS")
+12 -13
View File
@@ -9,13 +9,12 @@ import (
"strconv" "strconv"
"time" "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/prometheus/client_golang/prometheus/promhttp"
"github.com/codegangsta/negroni" "github.com/codegangsta/negroni"
@@ -37,13 +36,13 @@ func main() {
} }
defer db.Close() defer db.Close()
bookRepo := book.NewMySQLRepository(db) bookRepo := repository.NewBookMySQL(db)
bookManager := book.NewManager(bookRepo) bookService := book.NewService(bookRepo)
userRepo := user.NewMySQLRepoRepository(db) userRepo := repository.NewUserMySQL(db)
userManager := user.NewManager(userRepo, password.NewService()) userService := user.NewService(userRepo)
loanUseCase := loan.NewUseCase(userManager, bookManager) loanUseCase := loan.NewService(userService, bookService)
metricService, err := metric.NewPrometheusService() metricService, err := metric.NewPrometheusService()
if err != nil { if err != nil {
@@ -57,13 +56,13 @@ func main() {
negroni.NewLogger(), negroni.NewLogger(),
) )
//book //book
handler.MakeBookHandlers(r, *n, bookManager) handler.MakeBookHandlers(r, *n, bookService)
//user //user
handler.MakeUserHandlers(r, *n, userManager) handler.MakeUserHandlers(r, *n, userService)
//loan //loan
handler.MakeLoanHandlers(r, *n, bookManager, userManager, loanUseCase) handler.MakeLoanHandlers(r, *n, bookService, userService, loanUseCase)
http.Handle("/", r) http.Handle("/", r)
http.Handle("/metrics", promhttp.Handler()) http.Handle("/metrics", promhttp.Handler())
+1 -1
View File
@@ -1,7 +1,7 @@
package presenter package presenter
import ( import (
"github.com/eminetto/clean-architecture-go-v2/domain/entity" "github.com/eminetto/clean-architecture-go-v2/entity"
) )
//Book data //Book data
+1 -1
View File
@@ -1,7 +1,7 @@
package presenter package presenter
import ( import (
"github.com/eminetto/clean-architecture-go-v2/domain/entity" "github.com/eminetto/clean-architecture-go-v2/entity"
) )
//User data //User data
+6 -5
View File
@@ -7,11 +7,12 @@ import (
"log" "log"
"os" "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/eminetto/clean-architecture-go-v2/config"
_ "github.com/go-sql-driver/mysql" _ "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" "github.com/eminetto/clean-architecture-go-v2/pkg/metric"
) )
@@ -40,9 +41,9 @@ func main() {
log.Fatal(err.Error()) log.Fatal(err.Error())
} }
defer db.Close() defer db.Close()
repo := book.NewMySQLRepository(db) repo := repository.NewBookMySQL(db)
manager := book.NewManager(repo) service := book.NewService(repo)
all, err := manager.Search(query) all, err := service.SearchBooks(query)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
+2 -2
View File
@@ -4,7 +4,7 @@ services:
image: mysql:5.7 image: mysql:5.7
command: --default-authentication-plugin=mysql_native_password --init-file /data/application/init.sql command: --default-authentication-plugin=mysql_native_password --init-file /data/application/init.sql
volumes: volumes:
- ./infra/init.sql:/data/application/init.sql - ./ops/init.sql:/data/application/init.sql
environment: environment:
MYSQL_ROOT_PASSWORD: clean_architecture_go_v2 MYSQL_ROOT_PASSWORD: clean_architecture_go_v2
MYSQL_DATABASE: clean_architecture_go_v2 MYSQL_DATABASE: clean_architecture_go_v2
@@ -30,7 +30,7 @@ services:
command: command:
- --config.file=/etc/prometheus/prometheus.yml - --config.file=/etc/prometheus/prometheus.yml
volumes: 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 container_name: clean-architecture-go-v2-prometheus
network_mode: "bridge" network_mode: "bridge"
prometheus-pushgateway: prometheus-pushgateway:
-18
View File
@@ -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
}
-18
View File
@@ -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(),
}
}
-28
View File
@@ -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
}
-55
View File
@@ -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)
}
-82
View File
@@ -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)
}
-78
View File
@@ -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
}
-19
View File
@@ -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
}
-18
View File
@@ -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(),
}
}
-28
View File
@@ -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
}
-71
View File
@@ -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)
}
-95
View File
@@ -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)
}
-78
View File
@@ -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
}
-12
View File
@@ -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
}
-100
View File
@@ -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
}
-101
View File
@@ -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)
})
}
@@ -1,28 +1,26 @@
package book package repository
import ( import (
"database/sql" "database/sql"
"time" "time"
"github.com/eminetto/clean-architecture-go-v2/domain" "github.com/eminetto/clean-architecture-go-v2/entity"
"github.com/eminetto/clean-architecture-go-v2/domain/entity"
) )
//mySQLRepo mysql repo //BookMySQL mysql repo
type mySQLRepo struct { type BookMySQL struct {
db *sql.DB db *sql.DB
} }
//NewMySQLRepository create new repository //NewBookMySQL create new repository
func NewMySQLRepository(db *sql.DB) *mySQLRepo { func NewBookMySQL(db *sql.DB) *BookMySQL {
return &mySQLRepo{ return &BookMySQL{
db: db, db: db,
} }
} }
//Create a book //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(` stmt, err := r.db.Prepare(`
insert into book (id, title, author, pages, quantity, created_at) insert into book (id, title, author, pages, quantity, created_at)
values(?,?,?,?,?,?)`) values(?,?,?,?,?,?)`)
@@ -48,12 +46,12 @@ func (r *mySQLRepo) Create(e *Book) (entity.ID, error) {
} }
//Get a book //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 = ?`) stmt, err := r.db.Prepare(`select id, title, author, pages, quantity, created_at from book where id = ?`)
if err != nil { if err != nil {
return nil, err return nil, err
} }
var b Book var b entity.Book
rows, err := stmt.Query(id) rows, err := stmt.Query(id)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -65,7 +63,7 @@ func (r *mySQLRepo) Get(id entity.ID) (*Book, error) {
} }
//Update a book //Update a book
func (r *mySQLRepo) Update(e *Book) error { func (r *BookMySQL) Update(e *entity.Book) error {
e.UpdatedAt = time.Now() 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) _, 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 { if err != nil {
@@ -75,57 +73,52 @@ func (r *mySQLRepo) Update(e *Book) error {
} }
//Search books //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 ?`) stmt, err := r.db.Prepare(`select id, title, author, pages, quantity, created_at from book where title like ?`)
if err != nil { if err != nil {
return nil, err return nil, err
} }
var books []*Book var books []*entity.Book
rows, err := stmt.Query("%" + query + "%") rows, err := stmt.Query("%" + query + "%")
if err != nil { if err != nil {
return nil, err return nil, err
} }
for rows.Next() { 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) err = rows.Scan(&b.ID, &b.Title, &b.Author, &b.Pages, &b.Quantity, &b.CreatedAt)
if err != nil { if err != nil {
return nil, err return nil, err
} }
books = append(books, &b) books = append(books, &b)
} }
if len(books) == 0 {
return nil, domain.ErrNotFound
}
return books, nil return books, nil
} }
//List books //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`) stmt, err := r.db.Prepare(`select id, title, author, pages, quantity, created_at from book`)
if err != nil { if err != nil {
return nil, err return nil, err
} }
var books []*Book var books []*entity.Book
rows, err := stmt.Query() rows, err := stmt.Query()
if err != nil { if err != nil {
return nil, err return nil, err
} }
for rows.Next() { 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) err = rows.Scan(&b.ID, &b.Title, &b.Author, &b.Pages, &b.Quantity, &b.CreatedAt)
if err != nil { if err != nil {
return nil, err return nil, err
} }
books = append(books, &b) books = append(books, &b)
} }
if len(books) == 0 {
return nil, domain.ErrNotFound
}
return books, nil return books, nil
} }
//Delete a book //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) _, err := r.db.Exec("delete from book where id = ?", id)
if err != nil { if err != nil {
return err return err
@@ -1,28 +1,27 @@
package user package repository
import ( import (
"database/sql" "database/sql"
"fmt"
"time" "time"
"github.com/eminetto/clean-architecture-go-v2/domain" "github.com/eminetto/clean-architecture-go-v2/entity"
"github.com/eminetto/clean-architecture-go-v2/domain/entity"
) )
//MySQLRepo mysql repo //UserMySQL mysql repo
type MySQLRepo struct { type UserMySQL struct {
db *sql.DB db *sql.DB
} }
//NewMySQLRepoRepository create new repository //NewUserMySQL create new repository
func NewMySQLRepoRepository(db *sql.DB) *MySQLRepo { func NewUserMySQL(db *sql.DB) *UserMySQL {
return &MySQLRepo{ return &UserMySQL{
db: db, db: db,
} }
} }
//Create an user //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(` stmt, err := r.db.Prepare(`
insert into user (id, email, password, first_name, last_name, created_at) insert into user (id, email, password, first_name, last_name, created_at)
values(?,?,?,?,?,?)`) values(?,?,?,?,?,?)`)
@@ -48,16 +47,16 @@ func (r *MySQLRepo) Create(e *User) (entity.ID, error) {
} }
//Get an user //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) 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 = ?`) stmt, err := db.Prepare(`select id, email, first_name, last_name, created_at from user where id = ?`)
if err != nil { if err != nil {
return nil, err return nil, err
} }
var u User var u entity.User
rows, err := stmt.Query(id) rows, err := stmt.Query(id)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -82,7 +81,7 @@ func getUser(id entity.ID, db *sql.DB) (*User, error) {
} }
//Update an user //Update an user
func (r *MySQLRepo) Update(e *User) error { func (r *UserMySQL) Update(e *entity.User) error {
e.UpdatedAt = time.Now() 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) _, 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 { if err != nil {
@@ -102,7 +101,7 @@ func (r *MySQLRepo) Update(e *User) error {
} }
//Search users //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 ?`) stmt, err := r.db.Prepare(`select id from user where name like ?`)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -122,9 +121,9 @@ func (r *MySQLRepo) Search(query string) ([]*User, error) {
ids = append(ids, i) ids = append(ids, i)
} }
if len(ids) == 0 { 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 { for _, id := range ids {
u, err := getUser(id, r.db) u, err := getUser(id, r.db)
if err != nil { if err != nil {
@@ -136,7 +135,7 @@ func (r *MySQLRepo) Search(query string) ([]*User, error) {
} }
//List users //List users
func (r *MySQLRepo) List() ([]*User, error) { func (r *UserMySQL) List() ([]*entity.User, error) {
stmt, err := r.db.Prepare(`select id from user`) stmt, err := r.db.Prepare(`select id from user`)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -156,9 +155,9 @@ func (r *MySQLRepo) List() ([]*User, error) {
ids = append(ids, i) ids = append(ids, i)
} }
if len(ids) == 0 { 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 { for _, id := range ids {
u, err := getUser(id, r.db) u, err := getUser(id, r.db)
if err != nil { if err != nil {
@@ -170,7 +169,7 @@ func (r *MySQLRepo) List() ([]*User, error) {
} }
//Delete an user //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) _, err := r.db.Exec("delete from user where id = ?", id)
if err != nil { if err != nil {
return err return err
+41
View File
@@ -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
}
+69
View File
@@ -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)
}
}
+3 -2
View File
@@ -2,15 +2,16 @@ package entity
import "github.com/google/uuid" import "github.com/google/uuid"
//ID entity ID
type ID = uuid.UUID type ID = uuid.UUID
//NewID create a new entity ID
func NewID() ID { func NewID() ID {
return ID(uuid.New()) return ID(uuid.New())
} }
//StringToID convert a string to an entity ID
func StringToID(s string) (ID, error) { func StringToID(s string) (ID, error) {
id, err := uuid.Parse(s) id, err := uuid.Parse(s)
return ID(id), err return ID(id), err
} }
+4 -1
View File
@@ -1,10 +1,13 @@
package domain package entity
import "errors" import "errors"
//ErrNotFound not found //ErrNotFound not found
var ErrNotFound = errors.New("Not found") var ErrNotFound = errors.New("Not found")
//ErrInvalidEntity invalid entity
var ErrInvalidEntity = errors.New("Invalid entity")
//ErrCannotBeDeleted cannot be deleted //ErrCannotBeDeleted cannot be deleted
var ErrCannotBeDeleted = errors.New("Cannot Be Deleted") var ErrCannotBeDeleted = errors.New("Cannot Be Deleted")
+97
View File
@@ -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
}
+111
View File
@@ -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)
}
}
+2 -1
View File
@@ -7,7 +7,7 @@ require (
github.com/codegangsta/negroni v0.3.0 github.com/codegangsta/negroni v0.3.0
github.com/eminetto/clean-architecture-go v0.0.0-20200312132632-3195811c74a3 github.com/eminetto/clean-architecture-go v0.0.0-20200312132632-3195811c74a3
github.com/go-sql-driver/mysql v1.5.0 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/google/uuid v1.1.1
github.com/gorilla/context v1.1.1 github.com/gorilla/context v1.1.1
github.com/gorilla/mux v1.6.2 github.com/gorilla/mux v1.6.2
@@ -17,4 +17,5 @@ require (
github.com/stretchr/testify v1.6.1 github.com/stretchr/testify v1.6.1
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce
rsc.io/quote/v3 v3.1.0 // indirect
) )
+2
View File
@@ -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.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 h1:GV+pQPG/EUUbkh47niozDcADz6go/dUwhVzdUQHIVRw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 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.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.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
View File
+73
View File
@@ -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
}
+35
View File
@@ -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
}
@@ -1,12 +1,11 @@
// Code generated by MockGen. DO NOT EDIT. // 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 is a generated GoMock package.
package mock package mock
import ( import (
entity "github.com/eminetto/clean-architecture-go-v2/domain/entity" entity "github.com/eminetto/clean-architecture-go-v2/entity"
book "github.com/eminetto/clean-architecture-go-v2/domain/entity/book"
gomock "github.com/golang/mock/gomock" gomock "github.com/golang/mock/gomock"
reflect "reflect" reflect "reflect"
) )
@@ -35,10 +34,10 @@ func (m *MockReader) EXPECT() *MockReaderMockRecorder {
} }
// Get mocks base method // 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() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Get", id) ret := m.ctrl.Call(m, "Get", id)
ret0, _ := ret[0].(*book.Book) ret0, _ := ret[0].(*entity.Book)
ret1, _ := ret[1].(error) ret1, _ := ret[1].(error)
return ret0, ret1 return ret0, ret1
} }
@@ -50,10 +49,10 @@ func (mr *MockReaderMockRecorder) Get(id interface{}) *gomock.Call {
} }
// Search mocks base method // 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() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Search", query) ret := m.ctrl.Call(m, "Search", query)
ret0, _ := ret[0].([]*book.Book) ret0, _ := ret[0].([]*entity.Book)
ret1, _ := ret[1].(error) ret1, _ := ret[1].(error)
return ret0, ret1 return ret0, ret1
} }
@@ -65,10 +64,10 @@ func (mr *MockReaderMockRecorder) Search(query interface{}) *gomock.Call {
} }
// List mocks base method // List mocks base method
func (m *MockReader) List() ([]*book.Book, error) { func (m *MockReader) List() ([]*entity.Book, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List") ret := m.ctrl.Call(m, "List")
ret0, _ := ret[0].([]*book.Book) ret0, _ := ret[0].([]*entity.Book)
ret1, _ := ret[1].(error) ret1, _ := ret[1].(error)
return ret0, ret1 return ret0, ret1
} }
@@ -103,7 +102,7 @@ func (m *MockWriter) EXPECT() *MockWriterMockRecorder {
} }
// Create mocks base method // 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() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Create", e) ret := m.ctrl.Call(m, "Create", e)
ret0, _ := ret[0].(entity.ID) ret0, _ := ret[0].(entity.ID)
@@ -118,7 +117,7 @@ func (mr *MockWriterMockRecorder) Create(e interface{}) *gomock.Call {
} }
// Update mocks base method // Update mocks base method
func (m *MockWriter) Update(e *book.Book) error { func (m *MockWriter) Update(e *entity.Book) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Update", e) ret := m.ctrl.Call(m, "Update", e)
ret0, _ := ret[0].(error) ret0, _ := ret[0].(error)
@@ -169,10 +168,10 @@ func (m *MockRepository) EXPECT() *MockRepositoryMockRecorder {
} }
// Get mocks base method // 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() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Get", id) ret := m.ctrl.Call(m, "Get", id)
ret0, _ := ret[0].(*book.Book) ret0, _ := ret[0].(*entity.Book)
ret1, _ := ret[1].(error) ret1, _ := ret[1].(error)
return ret0, ret1 return ret0, ret1
} }
@@ -184,10 +183,10 @@ func (mr *MockRepositoryMockRecorder) Get(id interface{}) *gomock.Call {
} }
// Search mocks base method // 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() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Search", query) ret := m.ctrl.Call(m, "Search", query)
ret0, _ := ret[0].([]*book.Book) ret0, _ := ret[0].([]*entity.Book)
ret1, _ := ret[1].(error) ret1, _ := ret[1].(error)
return ret0, ret1 return ret0, ret1
} }
@@ -199,10 +198,10 @@ func (mr *MockRepositoryMockRecorder) Search(query interface{}) *gomock.Call {
} }
// List mocks base method // List mocks base method
func (m *MockRepository) List() ([]*book.Book, error) { func (m *MockRepository) List() ([]*entity.Book, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List") ret := m.ctrl.Call(m, "List")
ret0, _ := ret[0].([]*book.Book) ret0, _ := ret[0].([]*entity.Book)
ret1, _ := ret[1].(error) ret1, _ := ret[1].(error)
return ret0, ret1 return ret0, ret1
} }
@@ -214,7 +213,7 @@ func (mr *MockRepositoryMockRecorder) List() *gomock.Call {
} }
// Create mocks base method // 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() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Create", e) ret := m.ctrl.Call(m, "Create", e)
ret0, _ := ret[0].(entity.ID) ret0, _ := ret[0].(entity.ID)
@@ -229,7 +228,7 @@ func (mr *MockRepositoryMockRecorder) Create(e interface{}) *gomock.Call {
} }
// Update mocks base method // Update mocks base method
func (m *MockRepository) Update(e *book.Book) error { func (m *MockRepository) Update(e *entity.Book) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Update", e) ret := m.ctrl.Call(m, "Update", e)
ret0, _ := ret[0].(error) 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) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockRepository)(nil).Delete), id)
} }
// MockManager is a mock of Manager interface // MockUseCase is a mock of UseCase interface
type MockManager struct { type MockUseCase struct {
ctrl *gomock.Controller ctrl *gomock.Controller
recorder *MockManagerMockRecorder recorder *MockUseCaseMockRecorder
} }
// MockManagerMockRecorder is the mock recorder for MockManager // MockUseCaseMockRecorder is the mock recorder for MockUseCase
type MockManagerMockRecorder struct { type MockUseCaseMockRecorder struct {
mock *MockManager mock *MockUseCase
} }
// NewMockManager creates a new mock instance // NewMockUseCase creates a new mock instance
func NewMockManager(ctrl *gomock.Controller) *MockManager { func NewMockUseCase(ctrl *gomock.Controller) *MockUseCase {
mock := &MockManager{ctrl: ctrl} mock := &MockUseCase{ctrl: ctrl}
mock.recorder = &MockManagerMockRecorder{mock} mock.recorder = &MockUseCaseMockRecorder{mock}
return mock return mock
} }
// EXPECT returns an object that allows the caller to indicate expected use // 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 return m.recorder
} }
// Get mocks base method // GetBook mocks base method
func (m *MockManager) Get(id entity.ID) (*book.Book, error) { func (m *MockUseCase) GetBook(id entity.ID) (*entity.Book, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Get", id) ret := m.ctrl.Call(m, "GetBook", id)
ret0, _ := ret[0].(*book.Book) ret0, _ := ret[0].(*entity.Book)
ret1, _ := ret[1].(error) ret1, _ := ret[1].(error)
return ret0, ret1 return ret0, ret1
} }
// Get indicates an expected call of Get // GetBook indicates an expected call of GetBook
func (mr *MockManagerMockRecorder) Get(id interface{}) *gomock.Call { func (mr *MockUseCaseMockRecorder) GetBook(id interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() 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 // SearchBooks mocks base method
func (m *MockManager) Search(query string) ([]*book.Book, error) { func (m *MockUseCase) SearchBooks(query string) ([]*entity.Book, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Search", query) ret := m.ctrl.Call(m, "SearchBooks", query)
ret0, _ := ret[0].([]*book.Book) ret0, _ := ret[0].([]*entity.Book)
ret1, _ := ret[1].(error) ret1, _ := ret[1].(error)
return ret0, ret1 return ret0, ret1
} }
// Search indicates an expected call of Search // SearchBooks indicates an expected call of SearchBooks
func (mr *MockManagerMockRecorder) Search(query interface{}) *gomock.Call { func (mr *MockUseCaseMockRecorder) SearchBooks(query interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() 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 // ListBooks mocks base method
func (m *MockManager) List() ([]*book.Book, error) { func (m *MockUseCase) ListBooks() ([]*entity.Book, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List") ret := m.ctrl.Call(m, "ListBooks")
ret0, _ := ret[0].([]*book.Book) ret0, _ := ret[0].([]*entity.Book)
ret1, _ := ret[1].(error) ret1, _ := ret[1].(error)
return ret0, ret1 return ret0, ret1
} }
// List indicates an expected call of List // ListBooks indicates an expected call of ListBooks
func (mr *MockManagerMockRecorder) List() *gomock.Call { func (mr *MockUseCaseMockRecorder) ListBooks() *gomock.Call {
mr.mock.ctrl.T.Helper() 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 // CreateBook mocks base method
func (m *MockManager) Create(e *book.Book) (entity.ID, error) { func (m *MockUseCase) CreateBook(title, author string, pages, quantity int) (entity.ID, error) {
m.ctrl.T.Helper() 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) ret0, _ := ret[0].(entity.ID)
ret1, _ := ret[1].(error) ret1, _ := ret[1].(error)
return ret0, ret1 return ret0, ret1
} }
// Create indicates an expected call of Create // CreateBook indicates an expected call of CreateBook
func (mr *MockManagerMockRecorder) Create(e interface{}) *gomock.Call { func (mr *MockUseCaseMockRecorder) CreateBook(title, author, pages, quantity interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() 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 // UpdateBook mocks base method
func (m *MockManager) Update(e *book.Book) error { func (m *MockUseCase) UpdateBook(e *entity.Book) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Update", e) ret := m.ctrl.Call(m, "UpdateBook", e)
ret0, _ := ret[0].(error) ret0, _ := ret[0].(error)
return ret0 return ret0
} }
// Update indicates an expected call of Update // UpdateBook indicates an expected call of UpdateBook
func (mr *MockManagerMockRecorder) Update(e interface{}) *gomock.Call { func (mr *MockUseCaseMockRecorder) UpdateBook(e interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() 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 // DeleteBook mocks base method
func (m *MockManager) Delete(id entity.ID) error { func (m *MockUseCase) DeleteBook(id entity.ID) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", id) ret := m.ctrl.Call(m, "DeleteBook", id)
ret0, _ := ret[0].(error) ret0, _ := ret[0].(error)
return ret0 return ret0
} }
// Delete indicates an expected call of Delete // DeleteBook indicates an expected call of DeleteBook
func (mr *MockManagerMockRecorder) Delete(id interface{}) *gomock.Call { func (mr *MockUseCaseMockRecorder) DeleteBook(id interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() 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)
} }
+85
View File
@@ -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)
}
+92
View File
@@ -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)
}
+11
View File
@@ -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
}
@@ -1,12 +1,11 @@
// Code generated by MockGen. DO NOT EDIT. // 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 is a generated GoMock package.
package mock package mock
import ( import (
book "github.com/eminetto/clean-architecture-go-v2/domain/entity/book" entity "github.com/eminetto/clean-architecture-go-v2/entity"
user "github.com/eminetto/clean-architecture-go-v2/domain/entity/user"
gomock "github.com/golang/mock/gomock" gomock "github.com/golang/mock/gomock"
reflect "reflect" reflect "reflect"
) )
@@ -35,7 +34,7 @@ func (m *MockUseCase) EXPECT() *MockUseCaseMockRecorder {
} }
// Borrow mocks base method // 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() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Borrow", u, b) ret := m.ctrl.Call(m, "Borrow", u, b)
ret0, _ := ret[0].(error) ret0, _ := ret[0].(error)
@@ -49,7 +48,7 @@ func (mr *MockUseCaseMockRecorder) Borrow(u, b interface{}) *gomock.Call {
} }
// Return mocks base method // Return mocks base method
func (m *MockUseCase) Return(b *book.Book) error { func (m *MockUseCase) Return(b *entity.Book) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Return", b) ret := m.ctrl.Call(m, "Return", b)
ret0, _ := ret[0].(error) ret0, _ := ret[0].(error)
+97
View File
@@ -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
}
+129
View File
@@ -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)
})
}
+78
View File
@@ -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
}
+35
View File
@@ -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
}
@@ -1,12 +1,11 @@
// Code generated by MockGen. DO NOT EDIT. // 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 is a generated GoMock package.
package mock package mock
import ( import (
entity "github.com/eminetto/clean-architecture-go-v2/domain/entity" entity "github.com/eminetto/clean-architecture-go-v2/entity"
user "github.com/eminetto/clean-architecture-go-v2/domain/entity/user"
gomock "github.com/golang/mock/gomock" gomock "github.com/golang/mock/gomock"
reflect "reflect" reflect "reflect"
) )
@@ -35,10 +34,10 @@ func (m *MockReader) EXPECT() *MockReaderMockRecorder {
} }
// Get mocks base method // 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() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Get", id) ret := m.ctrl.Call(m, "Get", id)
ret0, _ := ret[0].(*user.User) ret0, _ := ret[0].(*entity.User)
ret1, _ := ret[1].(error) ret1, _ := ret[1].(error)
return ret0, ret1 return ret0, ret1
} }
@@ -50,10 +49,10 @@ func (mr *MockReaderMockRecorder) Get(id interface{}) *gomock.Call {
} }
// Search mocks base method // 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() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Search", query) ret := m.ctrl.Call(m, "Search", query)
ret0, _ := ret[0].([]*user.User) ret0, _ := ret[0].([]*entity.User)
ret1, _ := ret[1].(error) ret1, _ := ret[1].(error)
return ret0, ret1 return ret0, ret1
} }
@@ -65,10 +64,10 @@ func (mr *MockReaderMockRecorder) Search(query interface{}) *gomock.Call {
} }
// List mocks base method // List mocks base method
func (m *MockReader) List() ([]*user.User, error) { func (m *MockReader) List() ([]*entity.User, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List") ret := m.ctrl.Call(m, "List")
ret0, _ := ret[0].([]*user.User) ret0, _ := ret[0].([]*entity.User)
ret1, _ := ret[1].(error) ret1, _ := ret[1].(error)
return ret0, ret1 return ret0, ret1
} }
@@ -103,7 +102,7 @@ func (m *MockWriter) EXPECT() *MockWriterMockRecorder {
} }
// Create mocks base method // 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() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Create", e) ret := m.ctrl.Call(m, "Create", e)
ret0, _ := ret[0].(entity.ID) ret0, _ := ret[0].(entity.ID)
@@ -118,7 +117,7 @@ func (mr *MockWriterMockRecorder) Create(e interface{}) *gomock.Call {
} }
// Update mocks base method // Update mocks base method
func (m *MockWriter) Update(e *user.User) error { func (m *MockWriter) Update(e *entity.User) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Update", e) ret := m.ctrl.Call(m, "Update", e)
ret0, _ := ret[0].(error) ret0, _ := ret[0].(error)
@@ -169,10 +168,10 @@ func (m *MockRepository) EXPECT() *MockRepositoryMockRecorder {
} }
// Get mocks base method // 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() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Get", id) ret := m.ctrl.Call(m, "Get", id)
ret0, _ := ret[0].(*user.User) ret0, _ := ret[0].(*entity.User)
ret1, _ := ret[1].(error) ret1, _ := ret[1].(error)
return ret0, ret1 return ret0, ret1
} }
@@ -184,10 +183,10 @@ func (mr *MockRepositoryMockRecorder) Get(id interface{}) *gomock.Call {
} }
// Search mocks base method // 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() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Search", query) ret := m.ctrl.Call(m, "Search", query)
ret0, _ := ret[0].([]*user.User) ret0, _ := ret[0].([]*entity.User)
ret1, _ := ret[1].(error) ret1, _ := ret[1].(error)
return ret0, ret1 return ret0, ret1
} }
@@ -199,10 +198,10 @@ func (mr *MockRepositoryMockRecorder) Search(query interface{}) *gomock.Call {
} }
// List mocks base method // List mocks base method
func (m *MockRepository) List() ([]*user.User, error) { func (m *MockRepository) List() ([]*entity.User, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List") ret := m.ctrl.Call(m, "List")
ret0, _ := ret[0].([]*user.User) ret0, _ := ret[0].([]*entity.User)
ret1, _ := ret[1].(error) ret1, _ := ret[1].(error)
return ret0, ret1 return ret0, ret1
} }
@@ -214,7 +213,7 @@ func (mr *MockRepositoryMockRecorder) List() *gomock.Call {
} }
// Create mocks base method // 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() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Create", e) ret := m.ctrl.Call(m, "Create", e)
ret0, _ := ret[0].(entity.ID) ret0, _ := ret[0].(entity.ID)
@@ -229,7 +228,7 @@ func (mr *MockRepositoryMockRecorder) Create(e interface{}) *gomock.Call {
} }
// Update mocks base method // Update mocks base method
func (m *MockRepository) Update(e *user.User) error { func (m *MockRepository) Update(e *entity.User) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Update", e) ret := m.ctrl.Call(m, "Update", e)
ret0, _ := ret[0].(error) 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) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockRepository)(nil).Delete), id)
} }
// MockManager is a mock of Manager interface // MockUseCase is a mock of UseCase interface
type MockManager struct { type MockUseCase struct {
ctrl *gomock.Controller ctrl *gomock.Controller
recorder *MockManagerMockRecorder recorder *MockUseCaseMockRecorder
} }
// MockManagerMockRecorder is the mock recorder for MockManager // MockUseCaseMockRecorder is the mock recorder for MockUseCase
type MockManagerMockRecorder struct { type MockUseCaseMockRecorder struct {
mock *MockManager mock *MockUseCase
} }
// NewMockManager creates a new mock instance // NewMockUseCase creates a new mock instance
func NewMockManager(ctrl *gomock.Controller) *MockManager { func NewMockUseCase(ctrl *gomock.Controller) *MockUseCase {
mock := &MockManager{ctrl: ctrl} mock := &MockUseCase{ctrl: ctrl}
mock.recorder = &MockManagerMockRecorder{mock} mock.recorder = &MockUseCaseMockRecorder{mock}
return mock return mock
} }
// EXPECT returns an object that allows the caller to indicate expected use // 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 return m.recorder
} }
// Get mocks base method // GetUser mocks base method
func (m *MockManager) Get(id entity.ID) (*user.User, error) { func (m *MockUseCase) GetUser(id entity.ID) (*entity.User, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Get", id) ret := m.ctrl.Call(m, "GetUser", id)
ret0, _ := ret[0].(*user.User) ret0, _ := ret[0].(*entity.User)
ret1, _ := ret[1].(error) ret1, _ := ret[1].(error)
return ret0, ret1 return ret0, ret1
} }
// Get indicates an expected call of Get // GetUser indicates an expected call of GetUser
func (mr *MockManagerMockRecorder) Get(id interface{}) *gomock.Call { func (mr *MockUseCaseMockRecorder) GetUser(id interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() 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 // SearchUsers mocks base method
func (m *MockManager) Search(query string) ([]*user.User, error) { func (m *MockUseCase) SearchUsers(query string) ([]*entity.User, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Search", query) ret := m.ctrl.Call(m, "SearchUsers", query)
ret0, _ := ret[0].([]*user.User) ret0, _ := ret[0].([]*entity.User)
ret1, _ := ret[1].(error) ret1, _ := ret[1].(error)
return ret0, ret1 return ret0, ret1
} }
// Search indicates an expected call of Search // SearchUsers indicates an expected call of SearchUsers
func (mr *MockManagerMockRecorder) Search(query interface{}) *gomock.Call { func (mr *MockUseCaseMockRecorder) SearchUsers(query interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() 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 // ListUsers mocks base method
func (m *MockManager) List() ([]*user.User, error) { func (m *MockUseCase) ListUsers() ([]*entity.User, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "List") ret := m.ctrl.Call(m, "ListUsers")
ret0, _ := ret[0].([]*user.User) ret0, _ := ret[0].([]*entity.User)
ret1, _ := ret[1].(error) ret1, _ := ret[1].(error)
return ret0, ret1 return ret0, ret1
} }
// List indicates an expected call of List // ListUsers indicates an expected call of ListUsers
func (mr *MockManagerMockRecorder) List() *gomock.Call { func (mr *MockUseCaseMockRecorder) ListUsers() *gomock.Call {
mr.mock.ctrl.T.Helper() 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 // CreateUser mocks base method
func (m *MockManager) Create(e *user.User) (entity.ID, error) { func (m *MockUseCase) CreateUser(email, password, firstName, lastName string) (entity.ID, error) {
m.ctrl.T.Helper() 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) ret0, _ := ret[0].(entity.ID)
ret1, _ := ret[1].(error) ret1, _ := ret[1].(error)
return ret0, ret1 return ret0, ret1
} }
// Create indicates an expected call of Create // CreateUser indicates an expected call of CreateUser
func (mr *MockManagerMockRecorder) Create(e interface{}) *gomock.Call { func (mr *MockUseCaseMockRecorder) CreateUser(email, password, firstName, lastName interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() 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 // UpdateUser mocks base method
func (m *MockManager) Update(e *user.User) error { func (m *MockUseCase) UpdateUser(e *entity.User) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Update", e) ret := m.ctrl.Call(m, "UpdateUser", e)
ret0, _ := ret[0].(error) ret0, _ := ret[0].(error)
return ret0 return ret0
} }
// Update indicates an expected call of Update // UpdateUser indicates an expected call of UpdateUser
func (mr *MockManagerMockRecorder) Update(e interface{}) *gomock.Call { func (mr *MockUseCaseMockRecorder) UpdateUser(e interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() 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 // DeleteUser mocks base method
func (m *MockManager) Delete(id entity.ID) error { func (m *MockUseCase) DeleteUser(id entity.ID) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", id) ret := m.ctrl.Call(m, "DeleteUser", id)
ret0, _ := ret[0].(error) ret0, _ := ret[0].(error)
return ret0 return ret0
} }
// Delete indicates an expected call of Delete // DeleteUser indicates an expected call of DeleteUser
func (mr *MockManagerMockRecorder) Delete(id interface{}) *gomock.Call { func (mr *MockUseCaseMockRecorder) DeleteUser(id interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() 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)
} }
+69
View File
@@ -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)
}
+105
View File
@@ -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)
}