From 834ec7660574313b73b2eb664a216fedac9f63b5 Mon Sep 17 00:00:00 2001 From: david Date: Mon, 11 Sep 2023 17:37:10 +0700 Subject: [PATCH 1/7] 04: apply clean architecture --- internal/account/account_service.go | 40 ------------ internal/account/{ => dto}/account_dto.go | 2 +- .../account/{ => handler}/account_handler.go | 27 ++++---- .../{ => handler}/account_handler_test.go | 10 +-- .../account/repository/account_reposiroty.go | 39 +++++++++++ internal/account/service/account_service.go | 33 ++++++++++ internal/di/account_di.go | 11 ++-- internal/{entity => domain}/account.go | 11 +++- mocks/AccountRepository.go | 65 +++++++++++++++++++ mocks/AccountService.go | 14 ++-- 10 files changed, 180 insertions(+), 72 deletions(-) delete mode 100644 internal/account/account_service.go rename internal/account/{ => dto}/account_dto.go (98%) rename internal/account/{ => handler}/account_handler.go (67%) rename internal/account/{ => handler}/account_handler_test.go (83%) create mode 100644 internal/account/repository/account_reposiroty.go create mode 100644 internal/account/service/account_service.go rename internal/{entity => domain}/account.go (61%) create mode 100644 mocks/AccountRepository.go diff --git a/internal/account/account_service.go b/internal/account/account_service.go deleted file mode 100644 index 1ee216c..0000000 --- a/internal/account/account_service.go +++ /dev/null @@ -1,40 +0,0 @@ -package account - -import ( - "github.com/gaogao-asia/golang-template/internal/entity" - "github.com/gaogao-asia/golang-template/pkg/errs" - "gorm.io/gorm" -) - -type accountService struct { - db *gorm.DB -} - -func NewAccountService(db *gorm.DB) *accountService { - return &accountService{ - db: db, - } -} - -func (s *accountService) GetAccounts() ([]entity.Account, error) { - var accounts []entity.Account - err := s.db.Find(&accounts).Error - if err != nil { - return nil, errs.ErrDBFailed.WithErr(err) - } - - if len(accounts) == 0 { - return nil, errs.ErrUserNotExist - } - - return accounts, nil -} - -func (s *accountService) CreateAccount(account *entity.Account) error { - err := s.db.Create(&account).Error - if err != nil { - return errs.ErrDBFailed.WithErr(err) - } - - return nil -} diff --git a/internal/account/account_dto.go b/internal/account/dto/account_dto.go similarity index 98% rename from internal/account/account_dto.go rename to internal/account/dto/account_dto.go index 2d5a288..c94ec09 100644 --- a/internal/account/account_dto.go +++ b/internal/account/dto/account_dto.go @@ -1,4 +1,4 @@ -package account +package dto import "github.com/gaogao-asia/golang-template/pkg/errs" diff --git a/internal/account/account_handler.go b/internal/account/handler/account_handler.go similarity index 67% rename from internal/account/account_handler.go rename to internal/account/handler/account_handler.go index f8612b1..44a81b2 100644 --- a/internal/account/account_handler.go +++ b/internal/account/handler/account_handler.go @@ -1,19 +1,20 @@ -package account +package handler import ( "net/http" "strings" - "github.com/gaogao-asia/golang-template/internal/entity" + "github.com/gaogao-asia/golang-template/internal/account/dto" + "github.com/gaogao-asia/golang-template/internal/domain" "github.com/gaogao-asia/golang-template/internal/server/http/response" "github.com/gin-gonic/gin" ) type AccountHandler struct { - accountSrv entity.AccountService + accountSrv domain.AccountService } -func NewAccountHandler(accountSrv entity.AccountService) *AccountHandler { +func NewAccountHandler(accountSrv domain.AccountService) *AccountHandler { return &AccountHandler{ accountSrv: accountSrv, } @@ -27,15 +28,15 @@ func (h *AccountHandler) GetAccounts(c *gin.Context) { } c.JSON(http.StatusOK, response.ResponseBody{ - Data: GetAccountsResponse{ + Data: dto.GetAccountsResponse{ Accounts: toGetAccountsResponse(accounts), }, }) } // toGetAccountsResponse -func toGetAccountsResponse(data []entity.Account) []AccountResponse { - res := make([]AccountResponse, 0) +func toGetAccountsResponse(data []*domain.Account) []dto.AccountResponse { + res := make([]dto.AccountResponse, 0) for _, v := range data { res = append(res, toAccountsResponse(v)) } @@ -43,7 +44,7 @@ func toGetAccountsResponse(data []entity.Account) []AccountResponse { } func (h *AccountHandler) CreateAccount(c *gin.Context) { - req := CreateAccountRequest{} + req := dto.CreateAccountRequest{} err := c.ShouldBindJSON(&req) if err != nil { response.GeneralError(c, err) @@ -57,7 +58,7 @@ func (h *AccountHandler) CreateAccount(c *gin.Context) { } // create account in database - account := entity.Account{ + account := domain.Account{ Name: req.Name, Email: req.Email, } @@ -68,14 +69,14 @@ func (h *AccountHandler) CreateAccount(c *gin.Context) { } c.JSON(http.StatusOK, response.ResponseBody{ - Data: CreateAccountResponse{ - Account: toAccountsResponse(account), + Data: dto.CreateAccountResponse{ + Account: toAccountsResponse(&account), }, }) } -func toAccountsResponse(data entity.Account) AccountResponse { - res := AccountResponse{ +func toAccountsResponse(data *domain.Account) dto.AccountResponse { + res := dto.AccountResponse{ ID: data.ID, Name: data.Name, Email: data.Email, diff --git a/internal/account/account_handler_test.go b/internal/account/handler/account_handler_test.go similarity index 83% rename from internal/account/account_handler_test.go rename to internal/account/handler/account_handler_test.go index 6291c62..2f03635 100644 --- a/internal/account/account_handler_test.go +++ b/internal/account/handler/account_handler_test.go @@ -1,10 +1,10 @@ -package account +package handler import ( "net/http" "testing" - "github.com/gaogao-asia/golang-template/internal/entity" + "github.com/gaogao-asia/golang-template/internal/domain" "github.com/gaogao-asia/golang-template/mocks" "github.com/gaogao-asia/golang-template/pkg/test" "github.com/stretchr/testify/assert" @@ -13,15 +13,15 @@ import ( func TestGetAccounts(t *testing.T) { tests := []struct { name string - aService entity.AccountService + aService domain.AccountService expected string isError assert.ErrorAssertionFunc }{ { name: "Get list accounts", - aService: func() entity.AccountService { + aService: func() domain.AccountService { mockAsrv := mocks.NewAccountService(t) - mockAsrv.On("GetAccounts").Return([]entity.Account{ + mockAsrv.On("GetAccounts").Return([]domain.Account{ { ID: 1, Name: "Minh", diff --git a/internal/account/repository/account_reposiroty.go b/internal/account/repository/account_reposiroty.go new file mode 100644 index 0000000..325f8c6 --- /dev/null +++ b/internal/account/repository/account_reposiroty.go @@ -0,0 +1,39 @@ +package repository + +import ( + "github.com/gaogao-asia/golang-template/internal/domain" + "github.com/gaogao-asia/golang-template/pkg/errs" + "gorm.io/gorm" +) + +type accountRepository struct { + DB *gorm.DB +} + +func NewAccountRepository(db *gorm.DB) *accountRepository { + return &accountRepository{ + DB: db, + } +} + +func (r *accountRepository) Get() ([]*domain.Account, error) { + var accounts []*domain.Account + err := r.DB.Find(&accounts).Error + if err != nil { + return nil, errs.ErrDBFailed.WithErr(err) + } + + if len(accounts) == 0 { + return nil, errs.ErrUserNotExist + } + return accounts, nil +} + +func (r *accountRepository) Create(account *domain.Account) error { + err := r.DB.Create(&account).Error + if err != nil { + return errs.ErrDBFailed.WithErr(err) + } + + return nil +} diff --git a/internal/account/service/account_service.go b/internal/account/service/account_service.go new file mode 100644 index 0000000..4011476 --- /dev/null +++ b/internal/account/service/account_service.go @@ -0,0 +1,33 @@ +package service + +import ( + "github.com/gaogao-asia/golang-template/internal/domain" +) + +type accountService struct { + accountRepo domain.AccountRepository +} + +func NewAccountService(accountRepo domain.AccountRepository) *accountService { + return &accountService{ + accountRepo: accountRepo, + } +} + +func (s *accountService) GetAccounts() ([]*domain.Account, error) { + accounts, err := s.accountRepo.Get() + if err != nil { + return nil, err + } + + return accounts, nil +} + +func (s *accountService) CreateAccount(account *domain.Account) error { + err := s.accountRepo.Create(account) + if err != nil { + return err + } + + return nil +} diff --git a/internal/di/account_di.go b/internal/di/account_di.go index 118b9fa..f532556 100644 --- a/internal/di/account_di.go +++ b/internal/di/account_di.go @@ -1,11 +1,14 @@ package di import ( - "github.com/gaogao-asia/golang-template/internal/account" + "github.com/gaogao-asia/golang-template/internal/account/handler" + accrepo "github.com/gaogao-asia/golang-template/internal/account/repository" + accsrv "github.com/gaogao-asia/golang-template/internal/account/service" "gorm.io/gorm" ) -func InitAccountHandler(db *gorm.DB) *account.AccountHandler { - srv := account.NewAccountService(db) - return account.NewAccountHandler(srv) +func InitAccountHandler(db *gorm.DB) *handler.AccountHandler { + repo := accrepo.NewAccountRepository(db) + srv := accsrv.NewAccountService(repo) + return handler.NewAccountHandler(srv) } diff --git a/internal/entity/account.go b/internal/domain/account.go similarity index 61% rename from internal/entity/account.go rename to internal/domain/account.go index 8fa2981..bf5ccd8 100644 --- a/internal/entity/account.go +++ b/internal/domain/account.go @@ -1,4 +1,4 @@ -package entity +package domain type Account struct { ID int64 `gorm:"column:id"` @@ -9,7 +9,14 @@ type Account struct { //go:generate mockery --name AccountService --output ../../mocks type AccountService interface { - GetAccounts() ([]Account, error) + GetAccounts() ([]*Account, error) CreateAccount(account *Account) error } + +//go:generate mockery --name AccountRepository --output ../../mocks +type AccountRepository interface { + Get() ([]*Account, error) + + Create(account *Account) error +} diff --git a/mocks/AccountRepository.go b/mocks/AccountRepository.go new file mode 100644 index 0000000..55e6ecd --- /dev/null +++ b/mocks/AccountRepository.go @@ -0,0 +1,65 @@ +// Code generated by mockery v3.0.0-alpha.0. DO NOT EDIT. + +package mocks + +import ( + domain "github.com/gaogao-asia/golang-template/internal/domain" + mock "github.com/stretchr/testify/mock" +) + +// AccountRepository is an autogenerated mock type for the AccountRepository type +type AccountRepository struct { + mock.Mock +} + +// Create provides a mock function with given fields: account +func (_m *AccountRepository) Create(account *domain.Account) error { + ret := _m.Called(account) + + var r0 error + if rf, ok := ret.Get(0).(func(*domain.Account) error); ok { + r0 = rf(account) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Get provides a mock function with given fields: +func (_m *AccountRepository) Get() ([]*domain.Account, error) { + ret := _m.Called() + + var r0 []*domain.Account + if rf, ok := ret.Get(0).(func() []*domain.Account); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*domain.Account) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +type mockConstructorTestingTNewAccountRepository interface { + mock.TestingT + Cleanup(func()) +} + +// NewAccountRepository creates a new instance of AccountRepository. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewAccountRepository(t mockConstructorTestingTNewAccountRepository) *AccountRepository { + mock := &AccountRepository{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/AccountService.go b/mocks/AccountService.go index 30f959e..6911853 100644 --- a/mocks/AccountService.go +++ b/mocks/AccountService.go @@ -3,7 +3,7 @@ package mocks import ( - entity "github.com/gaogao-asia/golang-template/internal/entity" + domain "github.com/gaogao-asia/golang-template/internal/domain" mock "github.com/stretchr/testify/mock" ) @@ -13,11 +13,11 @@ type AccountService struct { } // CreateAccount provides a mock function with given fields: account -func (_m *AccountService) CreateAccount(account *entity.Account) error { +func (_m *AccountService) CreateAccount(account *domain.Account) error { ret := _m.Called(account) var r0 error - if rf, ok := ret.Get(0).(func(*entity.Account) error); ok { + if rf, ok := ret.Get(0).(func(*domain.Account) error); ok { r0 = rf(account) } else { r0 = ret.Error(0) @@ -27,15 +27,15 @@ func (_m *AccountService) CreateAccount(account *entity.Account) error { } // GetAccounts provides a mock function with given fields: -func (_m *AccountService) GetAccounts() ([]entity.Account, error) { +func (_m *AccountService) GetAccounts() ([]*domain.Account, error) { ret := _m.Called() - var r0 []entity.Account - if rf, ok := ret.Get(0).(func() []entity.Account); ok { + var r0 []*domain.Account + if rf, ok := ret.Get(0).(func() []*domain.Account); ok { r0 = rf() } else { if ret.Get(0) != nil { - r0 = ret.Get(0).([]entity.Account) + r0 = ret.Get(0).([]*domain.Account) } } From 04664e901d19711824a33f06509b6d0fc5913848 Mon Sep 17 00:00:00 2001 From: david Date: Tue, 12 Sep 2023 15:41:01 +0700 Subject: [PATCH 2/7] 04: add unit-test --- .mockery.yaml | 1 - Makefile | 2 +- go.mod | 1 + go.sum | 2 + internal/account/dto/account_dto.go | 14 +- internal/account/handler/account_handler.go | 9 +- .../account/handler/account_handler_test.go | 95 +++++++++++- .../account/repository/account_reposiroty.go | 10 +- .../repository/account_reposiroty_test.go | 146 ++++++++++++++++++ internal/account/service/account_service.go | 10 +- .../account/service/account_service_test.go | 119 ++++++++++++++ internal/domain/account.go | 12 +- mocks/AccountRepository.go | 105 ++++++++++--- mocks/AccountService.go | 105 ++++++++++--- pkg/requestbind/requestbind.go | 63 ++++++++ pkg/test/ginserver.go | 19 --- pkg/testutil/ginserver.go | 33 ++++ pkg/testutil/gorm_mock.go | 29 ++++ 18 files changed, 689 insertions(+), 86 deletions(-) create mode 100644 internal/account/repository/account_reposiroty_test.go create mode 100644 internal/account/service/account_service_test.go create mode 100644 pkg/requestbind/requestbind.go delete mode 100644 pkg/test/ginserver.go create mode 100644 pkg/testutil/ginserver.go create mode 100644 pkg/testutil/gorm_mock.go diff --git a/.mockery.yaml b/.mockery.yaml index 016e814..3cc46c5 100644 --- a/.mockery.yaml +++ b/.mockery.yaml @@ -1,4 +1,3 @@ outpkg: mocks -case: underscore name: "{{.MockName}}.go" with-expecter: true \ No newline at end of file diff --git a/Makefile b/Makefile index ff71a33..13a7f15 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ $(eval export $(shell sed -ne 's/ *#.*$$//; /./ s/=.*$$// p' .env)) init: docker compose -f starter/docker-compose.yaml up -d - go install github.com/vektra/mockery/v3@latest + go install github.com/vektra/mockery/v2@latest init/down: docker compose -f starter/docker-compose.yaml down run: diff --git a/go.mod b/go.mod index afbe1df..3553487 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( ) require ( + github.com/DATA-DOG/go-sqlmock v1.5.0 // indirect github.com/bytedance/sonic v1.10.0 // indirect github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect github.com/chenzhuoyu/iasm v0.9.0 // indirect diff --git a/go.sum b/go.sum index bc4ad92..6e71686 100644 --- a/go.sum +++ b/go.sum @@ -38,6 +38,8 @@ cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3f dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= +github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM= github.com/bytedance/sonic v1.10.0 h1:qtNZduETEIWJVIyDl01BeNxur2rW9OwTQ/yBqFRkKEk= diff --git a/internal/account/dto/account_dto.go b/internal/account/dto/account_dto.go index c94ec09..1c9d29e 100644 --- a/internal/account/dto/account_dto.go +++ b/internal/account/dto/account_dto.go @@ -2,18 +2,16 @@ package dto import "github.com/gaogao-asia/golang-template/pkg/errs" -type Role string - const ( - RoleAdmin Role = "admin" - RoleUser Role = "user" - RoleUnknow Role = "unknow" + RoleAdmin string = "admin" + RoleUser string = "user" + RoleUnknow string = "unknow" ) type CreateAccountRequest struct { - Name string `json:"name" binding:"required"` - Email string `json:"email" binding:"required,email"` - Roles []Role `json:"roles" binding:"required"` + Name string `json:"name" binding:"required"` + Email string `json:"email" binding:"required,email"` + Roles []string `json:"roles" binding:"required"` } func (s CreateAccountRequest) Validate() error { diff --git a/internal/account/handler/account_handler.go b/internal/account/handler/account_handler.go index 44a81b2..4ddb272 100644 --- a/internal/account/handler/account_handler.go +++ b/internal/account/handler/account_handler.go @@ -7,6 +7,7 @@ import ( "github.com/gaogao-asia/golang-template/internal/account/dto" "github.com/gaogao-asia/golang-template/internal/domain" "github.com/gaogao-asia/golang-template/internal/server/http/response" + "github.com/gaogao-asia/golang-template/pkg/requestbind" "github.com/gin-gonic/gin" ) @@ -21,7 +22,7 @@ func NewAccountHandler(accountSrv domain.AccountService) *AccountHandler { } func (h *AccountHandler) GetAccounts(c *gin.Context) { - accounts, err := h.accountSrv.GetAccounts() + accounts, err := h.accountSrv.GetAccounts(c.Request.Context()) if err != nil { response.GeneralError(c, err) return @@ -44,8 +45,7 @@ func toGetAccountsResponse(data []*domain.Account) []dto.AccountResponse { } func (h *AccountHandler) CreateAccount(c *gin.Context) { - req := dto.CreateAccountRequest{} - err := c.ShouldBindJSON(&req) + req, err := requestbind.BindJson[dto.CreateAccountRequest](c) if err != nil { response.GeneralError(c, err) return @@ -61,8 +61,9 @@ func (h *AccountHandler) CreateAccount(c *gin.Context) { account := domain.Account{ Name: req.Name, Email: req.Email, + Roles: strings.Join(req.Roles, ","), } - err = h.accountSrv.CreateAccount(&account) + err = h.accountSrv.CreateAccount(c.Request.Context(), &account) if err != nil { response.GeneralError(c, err) return diff --git a/internal/account/handler/account_handler_test.go b/internal/account/handler/account_handler_test.go index 2f03635..d2023b3 100644 --- a/internal/account/handler/account_handler_test.go +++ b/internal/account/handler/account_handler_test.go @@ -1,13 +1,15 @@ package handler import ( - "net/http" + "context" "testing" "github.com/gaogao-asia/golang-template/internal/domain" "github.com/gaogao-asia/golang-template/mocks" - "github.com/gaogao-asia/golang-template/pkg/test" + "github.com/gaogao-asia/golang-template/pkg/errs" + "github.com/gaogao-asia/golang-template/pkg/testutil" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" ) func TestGetAccounts(t *testing.T) { @@ -21,7 +23,7 @@ func TestGetAccounts(t *testing.T) { name: "Get list accounts", aService: func() domain.AccountService { mockAsrv := mocks.NewAccountService(t) - mockAsrv.On("GetAccounts").Return([]domain.Account{ + mockAsrv.On("GetAccounts", context.Background()).Return([]*domain.Account{ { ID: 1, Name: "Minh", @@ -34,16 +36,99 @@ func TestGetAccounts(t *testing.T) { expected: `{"data":{"accounts":[{"id":1,"name":"Minh","email":"minhtran.dn.it@gmail.com","roles":["admin"]}]}}`, isError: assert.NoError, }, + { + name: "get error", + aService: func() domain.AccountService { + mockAsrv := mocks.NewAccountService(t) + mockAsrv.On("GetAccounts", context.Background()).Return(nil, errs.ErrUserNotExist) + return mockAsrv + }(), + expected: `{"error":{"code":404001,"msg_code":"USER_NOT_EXIST"}}`, + isError: assert.Error, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { handler := NewAccountHandler(tt.aService) - ctx, resWriter := test.GetGinTestContext() + ctx, resWriter := testutil.GetGinTestContext() handler.GetAccounts(ctx) - assert.EqualValues(t, http.StatusOK, resWriter.Code) + resBody := resWriter.Body.String() + assert.EqualValues(t, tt.expected, resBody) + }) + } +} + +// TestCreateAccount +func TestCreateAccount(t *testing.T) { + tests := []struct { + name string + aService domain.AccountService + requestBody string + expected string + isError assert.ErrorAssertionFunc + }{ + { + name: "Create account successful, get account", + aService: func() domain.AccountService { + mockAsrv := mocks.NewAccountService(t) + mockAsrv.On("CreateAccount", mock.Anything, &domain.Account{ + Name: "Minh", + Email: "trainer.minhtran@gmail.com", + Roles: "admin", + }).Return(nil) + return mockAsrv + }(), + requestBody: `{"name":"Minh","email":"trainer.minhtran@gmail.com","roles":["admin"]}`, + expected: `{"data":{"account":{"name":"Minh","email":"trainer.minhtran@gmail.com","roles":["admin"]}}}`, + isError: assert.NoError, + }, + { + name: "Create account miss request field, get error", + aService: func() domain.AccountService { + mockAsrv := mocks.NewAccountService(t) + return mockAsrv + }(), + requestBody: `{"name":"Minh","email":"trainer.minhtran@gmail.com"}`, + expected: `{"error":{"code":400000,"msg_code":"BAD_REQUEST"}}`, + isError: assert.Error, + }, + { + name: "Create account wrong role, get error", + aService: func() domain.AccountService { + mockAsrv := mocks.NewAccountService(t) + + return mockAsrv + }(), + requestBody: `{"name":"Minh","email":"trainer.minhtran@gmail.com","roles":["fwef"]}`, + expected: `{"error":{"code":400001,"msg_code":"CREATE_ACCOUNT_REQUEST_ROLE_INVALID"}}`, + isError: assert.Error, + }, + { + name: "Create account service return error, get error", + aService: func() domain.AccountService { + mockAsrv := mocks.NewAccountService(t) + mockAsrv.On("CreateAccount", mock.Anything, &domain.Account{ + Name: "Minh", + Email: "trainer.minhtran@gmail.com", + Roles: "admin", + }).Return(errs.ErrBadRequest) + return mockAsrv + }(), + requestBody: `{"name":"Minh","email":"trainer.minhtran@gmail.com","roles":["admin"]}`, + expected: `{"error":{"code":400000,"msg_code":"BAD_REQUEST"}}`, + isError: assert.Error, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + handler := NewAccountHandler(tt.aService) + + ctx, resWriter := testutil.GetGinTestContextWithBody(tt.requestBody) + handler.CreateAccount(ctx) resBody := resWriter.Body.String() assert.EqualValues(t, tt.expected, resBody) diff --git a/internal/account/repository/account_reposiroty.go b/internal/account/repository/account_reposiroty.go index 325f8c6..dbef915 100644 --- a/internal/account/repository/account_reposiroty.go +++ b/internal/account/repository/account_reposiroty.go @@ -1,6 +1,8 @@ package repository import ( + "context" + "github.com/gaogao-asia/golang-template/internal/domain" "github.com/gaogao-asia/golang-template/pkg/errs" "gorm.io/gorm" @@ -16,9 +18,9 @@ func NewAccountRepository(db *gorm.DB) *accountRepository { } } -func (r *accountRepository) Get() ([]*domain.Account, error) { +func (r *accountRepository) Get(ctx context.Context) ([]*domain.Account, error) { var accounts []*domain.Account - err := r.DB.Find(&accounts).Error + err := r.DB.Debug().WithContext(ctx).Find(&accounts).Error if err != nil { return nil, errs.ErrDBFailed.WithErr(err) } @@ -29,8 +31,8 @@ func (r *accountRepository) Get() ([]*domain.Account, error) { return accounts, nil } -func (r *accountRepository) Create(account *domain.Account) error { - err := r.DB.Create(&account).Error +func (r *accountRepository) Create(ctx context.Context, account *domain.Account) error { + err := r.DB.Debug().Create(&account).Error if err != nil { return errs.ErrDBFailed.WithErr(err) } diff --git a/internal/account/repository/account_reposiroty_test.go b/internal/account/repository/account_reposiroty_test.go new file mode 100644 index 0000000..c2ac99f --- /dev/null +++ b/internal/account/repository/account_reposiroty_test.go @@ -0,0 +1,146 @@ +package repository + +import ( + "context" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/gaogao-asia/golang-template/internal/domain" + "github.com/gaogao-asia/golang-template/pkg/testutil" + "github.com/stretchr/testify/assert" + "gorm.io/gorm" +) + +func TestAccountRepositoryGet(t *testing.T) { + tests := []struct { + name string + db *gorm.DB + want []*domain.Account + wantErr assert.ErrorAssertionFunc + }{ + { + name: "success, get accounts", + db: func() *gorm.DB { + db, mock := testutil.GetGORMMock() + + columns := []string{"id", "name", "email", "roles"} + mock.ExpectQuery(`SELECT \* FROM \"accounts\"`). + WillReturnRows(sqlmock.NewRows(columns). + AddRow(1, "gaogao", "gao.gao@gmail.com", "admin,user"). + AddRow(2, "minh", "trainer.minhtran@gmail.com", "user")) + + return db + }(), + want: []*domain.Account{ + { + ID: 1, + Name: "gaogao", + Email: "gao.gao@gmail.com", + Roles: "admin,user", + }, + { + ID: 2, + Name: "minh", + Email: "trainer.minhtran@gmail.com", + Roles: "user", + }, + }, + wantErr: assert.NoError, + }, + { + name: "success, no account found", + db: func() *gorm.DB { + db, mock := testutil.GetGORMMock() + + columns := []string{"id", "name", "email", "roles"} + mock.ExpectQuery(`SELECT \* FROM \"accounts\"`). + WillReturnRows(sqlmock.NewRows(columns)) + + return db + }(), + want: nil, + wantErr: assert.Error, + }, + { + name: "failed, get error from db", + db: func() *gorm.DB { + db, mock := testutil.GetGORMMock() + + mock.ExpectQuery(`SELECT \* FROM \"accounts\"`). + WillReturnError(assert.AnError) + + return db + }(), + want: nil, + wantErr: assert.Error, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + repo := NewAccountRepository(tt.db) + + got, err := repo.Get(context.Background()) + + tt.wantErr(t, err) + assert.EqualValues(t, tt.want, got) + }) + } +} + +// write unittest for Create +func TestAccountRepositoryCreate(t *testing.T) { + tests := []struct { + name string + input *domain.Account + db *gorm.DB + wantErr assert.ErrorAssertionFunc + }{ + { + name: "success, create account", + input: &domain.Account{ + Name: "gaogao", + Email: "trainer.minhtran@gmail.com", + Roles: "admin,user", + }, + db: func() *gorm.DB { + db, mock := testutil.GetGORMMock() + + mock.ExpectQuery(`^INSERT INTO "accounts" \("name","email","roles"\) VALUES \(\$1,\$2,\$3\) RETURNING "id"$`). + WithArgs("gaogao", "trainer.minhtran@gmail.com", "admin,user"). + WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(1)) + + return db + }(), + + wantErr: assert.NoError, + }, + { + name: "failed, create account", + input: &domain.Account{ + Name: "gaogao", + Email: "trainer.minhtran@gmail.com", + Roles: "admin,user", + }, + db: func() *gorm.DB { + db, mock := testutil.GetGORMMock() + + mock.ExpectQuery(`^INSERT INTO "accounts" \("name","email","roles"\) VALUES \(\$1,\$2,\$3\) RETURNING "id"$`). + WithArgs("gaogao", "trainer.minhtran@gmail.com", "admin,user"). + WillReturnError(assert.AnError) + + return db + }(), + wantErr: assert.Error, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + repo := NewAccountRepository(tt.db) + + err := repo.Create(context.Background(), tt.input) + tt.wantErr(t, err) + }) + } +} diff --git a/internal/account/service/account_service.go b/internal/account/service/account_service.go index 4011476..2e4e5a9 100644 --- a/internal/account/service/account_service.go +++ b/internal/account/service/account_service.go @@ -1,6 +1,8 @@ package service import ( + "context" + "github.com/gaogao-asia/golang-template/internal/domain" ) @@ -14,8 +16,8 @@ func NewAccountService(accountRepo domain.AccountRepository) *accountService { } } -func (s *accountService) GetAccounts() ([]*domain.Account, error) { - accounts, err := s.accountRepo.Get() +func (s *accountService) GetAccounts(ctx context.Context) ([]*domain.Account, error) { + accounts, err := s.accountRepo.Get(ctx) if err != nil { return nil, err } @@ -23,8 +25,8 @@ func (s *accountService) GetAccounts() ([]*domain.Account, error) { return accounts, nil } -func (s *accountService) CreateAccount(account *domain.Account) error { - err := s.accountRepo.Create(account) +func (s *accountService) CreateAccount(ctx context.Context, account *domain.Account) error { + err := s.accountRepo.Create(ctx, account) if err != nil { return err } diff --git a/internal/account/service/account_service_test.go b/internal/account/service/account_service_test.go new file mode 100644 index 0000000..adb2f13 --- /dev/null +++ b/internal/account/service/account_service_test.go @@ -0,0 +1,119 @@ +package service + +import ( + "context" + "testing" + + "github.com/gaogao-asia/golang-template/internal/domain" + "github.com/gaogao-asia/golang-template/mocks" + "github.com/gaogao-asia/golang-template/pkg/errs" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestGetAccounts(t *testing.T) { + tests := []struct { + name string + aRepo domain.AccountRepository + expected []*domain.Account + isError assert.ErrorAssertionFunc + }{ + { + name: "success, get accounts", + aRepo: func() domain.AccountRepository { + mockARepo := mocks.NewAccountRepository(t) + mockARepo.On("Get", context.Background()).Return([]*domain.Account{ + { + ID: 1, + Name: "gaogao", + Email: "trainer.minhtran@gmail.com", + Roles: "admin,user", + }, + }, nil) + return mockARepo + }(), + expected: []*domain.Account{ + { + ID: 1, + Name: "gaogao", + Email: "trainer.minhtran@gmail.com", + Roles: "admin,user", + }, + }, + isError: assert.NoError, + }, + { + name: "error, get error", + aRepo: func() domain.AccountRepository { + mockARepo := mocks.NewAccountRepository(t) + mockARepo.On("Get", context.Background()).Return(nil, errs.ErrDBFailed) + return mockARepo + }(), + expected: nil, + isError: assert.Error, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + srv := NewAccountService(tt.aRepo) + + result, err := srv.GetAccounts(context.Background()) + tt.isError(t, err) + assert.EqualValues(t, tt.expected, result) + }) + } +} + +func TestCreateAccount(t *testing.T) { + tests := []struct { + name string + aRepo domain.AccountRepository + requestParam *domain.Account + isError assert.ErrorAssertionFunc + }{ + { + name: "success, get account", + requestParam: &domain.Account{ + Name: "gaogao", + Email: "trainer.minhtran@gmail.com", + Roles: "admin,user", + }, + aRepo: func() domain.AccountRepository { + mockARepo := mocks.NewAccountRepository(t) + mockARepo.On("Create", mock.Anything, &domain.Account{ + Name: "gaogao", + Email: "trainer.minhtran@gmail.com", + Roles: "admin,user", + }).Return(nil) + return mockARepo + }(), + isError: assert.NoError, + }, + { + name: "error, get error", + requestParam: &domain.Account{ + Name: "gaogao", + Email: "trainer.minhtran@gmail.com", + Roles: "admin,user", + }, + aRepo: func() domain.AccountRepository { + mockARepo := mocks.NewAccountRepository(t) + mockARepo.On("Create", mock.Anything, &domain.Account{ + Name: "gaogao", + Email: "trainer.minhtran@gmail.com", + Roles: "admin,user", + }).Return(errs.ErrDBFailed) + return mockARepo + }(), + isError: assert.Error, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + srv := NewAccountService(tt.aRepo) + + err := srv.CreateAccount(context.Background(), tt.requestParam) + tt.isError(t, err) + }) + } +} diff --git a/internal/domain/account.go b/internal/domain/account.go index bf5ccd8..0ee0c75 100644 --- a/internal/domain/account.go +++ b/internal/domain/account.go @@ -1,5 +1,9 @@ package domain +import ( + "context" +) + type Account struct { ID int64 `gorm:"column:id"` Name string `gorm:"column:name"` @@ -9,14 +13,14 @@ type Account struct { //go:generate mockery --name AccountService --output ../../mocks type AccountService interface { - GetAccounts() ([]*Account, error) + GetAccounts(ctx context.Context) ([]*Account, error) - CreateAccount(account *Account) error + CreateAccount(ctx context.Context, acc *Account) error } //go:generate mockery --name AccountRepository --output ../../mocks type AccountRepository interface { - Get() ([]*Account, error) + Get(ctx context.Context) ([]*Account, error) - Create(account *Account) error + Create(ctx context.Context, acc *Account) error } diff --git a/mocks/AccountRepository.go b/mocks/AccountRepository.go index 55e6ecd..010daba 100644 --- a/mocks/AccountRepository.go +++ b/mocks/AccountRepository.go @@ -1,8 +1,10 @@ -// Code generated by mockery v3.0.0-alpha.0. DO NOT EDIT. +// Code generated by mockery v2.33.2. DO NOT EDIT. package mocks import ( + context "context" + domain "github.com/gaogao-asia/golang-template/internal/domain" mock "github.com/stretchr/testify/mock" ) @@ -12,13 +14,21 @@ type AccountRepository struct { mock.Mock } -// Create provides a mock function with given fields: account -func (_m *AccountRepository) Create(account *domain.Account) error { - ret := _m.Called(account) +type AccountRepository_Expecter struct { + mock *mock.Mock +} + +func (_m *AccountRepository) EXPECT() *AccountRepository_Expecter { + return &AccountRepository_Expecter{mock: &_m.Mock} +} + +// Create provides a mock function with given fields: ctx, acc +func (_m *AccountRepository) Create(ctx context.Context, acc *domain.Account) error { + ret := _m.Called(ctx, acc) var r0 error - if rf, ok := ret.Get(0).(func(*domain.Account) error); ok { - r0 = rf(account) + if rf, ok := ret.Get(0).(func(context.Context, *domain.Account) error); ok { + r0 = rf(ctx, acc) } else { r0 = ret.Error(0) } @@ -26,22 +36,54 @@ func (_m *AccountRepository) Create(account *domain.Account) error { return r0 } -// Get provides a mock function with given fields: -func (_m *AccountRepository) Get() ([]*domain.Account, error) { - ret := _m.Called() +// AccountRepository_Create_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Create' +type AccountRepository_Create_Call struct { + *mock.Call +} + +// Create is a helper method to define mock.On call +// - ctx context.Context +// - acc *domain.Account +func (_e *AccountRepository_Expecter) Create(ctx interface{}, acc interface{}) *AccountRepository_Create_Call { + return &AccountRepository_Create_Call{Call: _e.mock.On("Create", ctx, acc)} +} + +func (_c *AccountRepository_Create_Call) Run(run func(ctx context.Context, acc *domain.Account)) *AccountRepository_Create_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*domain.Account)) + }) + return _c +} + +func (_c *AccountRepository_Create_Call) Return(_a0 error) *AccountRepository_Create_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *AccountRepository_Create_Call) RunAndReturn(run func(context.Context, *domain.Account) error) *AccountRepository_Create_Call { + _c.Call.Return(run) + return _c +} + +// Get provides a mock function with given fields: ctx +func (_m *AccountRepository) Get(ctx context.Context) ([]*domain.Account, error) { + ret := _m.Called(ctx) var r0 []*domain.Account - if rf, ok := ret.Get(0).(func() []*domain.Account); ok { - r0 = rf() + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) ([]*domain.Account, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) []*domain.Account); ok { + r0 = rf(ctx) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]*domain.Account) } } - var r1 error - if rf, ok := ret.Get(1).(func() error); ok { - r1 = rf() + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) } else { r1 = ret.Error(1) } @@ -49,13 +91,40 @@ func (_m *AccountRepository) Get() ([]*domain.Account, error) { return r0, r1 } -type mockConstructorTestingTNewAccountRepository interface { - mock.TestingT - Cleanup(func()) +// AccountRepository_Get_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Get' +type AccountRepository_Get_Call struct { + *mock.Call +} + +// Get is a helper method to define mock.On call +// - ctx context.Context +func (_e *AccountRepository_Expecter) Get(ctx interface{}) *AccountRepository_Get_Call { + return &AccountRepository_Get_Call{Call: _e.mock.On("Get", ctx)} +} + +func (_c *AccountRepository_Get_Call) Run(run func(ctx context.Context)) *AccountRepository_Get_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *AccountRepository_Get_Call) Return(_a0 []*domain.Account, _a1 error) *AccountRepository_Get_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *AccountRepository_Get_Call) RunAndReturn(run func(context.Context) ([]*domain.Account, error)) *AccountRepository_Get_Call { + _c.Call.Return(run) + return _c } // NewAccountRepository creates a new instance of AccountRepository. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewAccountRepository(t mockConstructorTestingTNewAccountRepository) *AccountRepository { +// The first argument is typically a *testing.T value. +func NewAccountRepository(t interface { + mock.TestingT + Cleanup(func()) +}) *AccountRepository { mock := &AccountRepository{} mock.Mock.Test(t) diff --git a/mocks/AccountService.go b/mocks/AccountService.go index 6911853..46cd81b 100644 --- a/mocks/AccountService.go +++ b/mocks/AccountService.go @@ -1,8 +1,10 @@ -// Code generated by mockery v3.0.0-alpha.0. DO NOT EDIT. +// Code generated by mockery v2.33.2. DO NOT EDIT. package mocks import ( + context "context" + domain "github.com/gaogao-asia/golang-template/internal/domain" mock "github.com/stretchr/testify/mock" ) @@ -12,13 +14,21 @@ type AccountService struct { mock.Mock } -// CreateAccount provides a mock function with given fields: account -func (_m *AccountService) CreateAccount(account *domain.Account) error { - ret := _m.Called(account) +type AccountService_Expecter struct { + mock *mock.Mock +} + +func (_m *AccountService) EXPECT() *AccountService_Expecter { + return &AccountService_Expecter{mock: &_m.Mock} +} + +// CreateAccount provides a mock function with given fields: ctx, acc +func (_m *AccountService) CreateAccount(ctx context.Context, acc *domain.Account) error { + ret := _m.Called(ctx, acc) var r0 error - if rf, ok := ret.Get(0).(func(*domain.Account) error); ok { - r0 = rf(account) + if rf, ok := ret.Get(0).(func(context.Context, *domain.Account) error); ok { + r0 = rf(ctx, acc) } else { r0 = ret.Error(0) } @@ -26,22 +36,54 @@ func (_m *AccountService) CreateAccount(account *domain.Account) error { return r0 } -// GetAccounts provides a mock function with given fields: -func (_m *AccountService) GetAccounts() ([]*domain.Account, error) { - ret := _m.Called() +// AccountService_CreateAccount_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateAccount' +type AccountService_CreateAccount_Call struct { + *mock.Call +} + +// CreateAccount is a helper method to define mock.On call +// - ctx context.Context +// - acc *domain.Account +func (_e *AccountService_Expecter) CreateAccount(ctx interface{}, acc interface{}) *AccountService_CreateAccount_Call { + return &AccountService_CreateAccount_Call{Call: _e.mock.On("CreateAccount", ctx, acc)} +} + +func (_c *AccountService_CreateAccount_Call) Run(run func(ctx context.Context, acc *domain.Account)) *AccountService_CreateAccount_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*domain.Account)) + }) + return _c +} + +func (_c *AccountService_CreateAccount_Call) Return(_a0 error) *AccountService_CreateAccount_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *AccountService_CreateAccount_Call) RunAndReturn(run func(context.Context, *domain.Account) error) *AccountService_CreateAccount_Call { + _c.Call.Return(run) + return _c +} + +// GetAccounts provides a mock function with given fields: ctx +func (_m *AccountService) GetAccounts(ctx context.Context) ([]*domain.Account, error) { + ret := _m.Called(ctx) var r0 []*domain.Account - if rf, ok := ret.Get(0).(func() []*domain.Account); ok { - r0 = rf() + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) ([]*domain.Account, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) []*domain.Account); ok { + r0 = rf(ctx) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]*domain.Account) } } - var r1 error - if rf, ok := ret.Get(1).(func() error); ok { - r1 = rf() + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) } else { r1 = ret.Error(1) } @@ -49,13 +91,40 @@ func (_m *AccountService) GetAccounts() ([]*domain.Account, error) { return r0, r1 } -type mockConstructorTestingTNewAccountService interface { - mock.TestingT - Cleanup(func()) +// AccountService_GetAccounts_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAccounts' +type AccountService_GetAccounts_Call struct { + *mock.Call +} + +// GetAccounts is a helper method to define mock.On call +// - ctx context.Context +func (_e *AccountService_Expecter) GetAccounts(ctx interface{}) *AccountService_GetAccounts_Call { + return &AccountService_GetAccounts_Call{Call: _e.mock.On("GetAccounts", ctx)} +} + +func (_c *AccountService_GetAccounts_Call) Run(run func(ctx context.Context)) *AccountService_GetAccounts_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *AccountService_GetAccounts_Call) Return(_a0 []*domain.Account, _a1 error) *AccountService_GetAccounts_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *AccountService_GetAccounts_Call) RunAndReturn(run func(context.Context) ([]*domain.Account, error)) *AccountService_GetAccounts_Call { + _c.Call.Return(run) + return _c } // NewAccountService creates a new instance of AccountService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewAccountService(t mockConstructorTestingTNewAccountService) *AccountService { +// The first argument is typically a *testing.T value. +func NewAccountService(t interface { + mock.TestingT + Cleanup(func()) +}) *AccountService { mock := &AccountService{} mock.Mock.Test(t) diff --git a/pkg/requestbind/requestbind.go b/pkg/requestbind/requestbind.go new file mode 100644 index 0000000..677d1b4 --- /dev/null +++ b/pkg/requestbind/requestbind.go @@ -0,0 +1,63 @@ +package requestbind + +import ( + "github.com/gaogao-asia/golang-template/pkg/errs" + "github.com/gaogao-asia/golang-template/pkg/utils" + "github.com/gin-gonic/gin" +) + +// BindJson bind json param to struct +// +// Example: /api/v1/users +// +// Body payload: {"phone": "123456789"} +func BindJson[T any](c *gin.Context) (*T, error) { + var req = new(T) + err := c.ShouldBindJSON(req) + if err != nil { + return nil, errs.ErrBadRequest.WithErr(err) + } + + paramMap := make(map[string]interface{}) + bytes, _ := utils.JsonMarshal(req) + _ = utils.JsonUnmarshal(bytes, ¶mMap) + if _, ok := paramMap["password"]; ok { + paramMap["password"] = "********" + } + + return req, nil +} + +// BindFormOrQuery bind form or query param to struct +// +// Example: /api/v1/users?phone=123456789 +func BindFormOrQuery[T any](c *gin.Context) (*T, error) { + var req = new(T) + err := c.ShouldBind(req) + if err != nil { + return nil, errs.ErrBadRequest.WithErr(err) + } + + paramMap := make(map[string]interface{}) + bytes, _ := utils.JsonMarshal(req) + _ = utils.JsonUnmarshal(bytes, ¶mMap) + + return req, nil +} + +// BindPathParam bind path param to struct +// +// Example: /api/v1/users/:id +func BindPathParam[T any](c *gin.Context) (*T, error) { + var req = new(T) + err := c.ShouldBindUri(req) + if err != nil { + return nil, errs.ErrBadRequest.WithErr(err) + } + + paramMap := make(map[string]interface{}) + bytes, _ := utils.JsonMarshal(req) + _ = utils.JsonUnmarshal(bytes, ¶mMap) + + return req, nil +} diff --git a/pkg/test/ginserver.go b/pkg/test/ginserver.go deleted file mode 100644 index abab86d..0000000 --- a/pkg/test/ginserver.go +++ /dev/null @@ -1,19 +0,0 @@ -package test - -import ( - "net/http" - "net/http/httptest" - - "github.com/gin-gonic/gin" -) - -func GetGinTestContext() (c *gin.Context, w *httptest.ResponseRecorder) { - // create a GIN context for test - w = httptest.NewRecorder() - ctx, _ := gin.CreateTestContext(w) - ctx.Request = &http.Request{ - Header: make(http.Header), - } - - return ctx, w -} diff --git a/pkg/testutil/ginserver.go b/pkg/testutil/ginserver.go new file mode 100644 index 0000000..e4b2de1 --- /dev/null +++ b/pkg/testutil/ginserver.go @@ -0,0 +1,33 @@ +package testutil + +import ( + "io" + "net/http" + "net/http/httptest" + "strings" + + "github.com/gin-gonic/gin" +) + +func GetGinTestContext() (c *gin.Context, w *httptest.ResponseRecorder) { + // create a GIN context for test + w = httptest.NewRecorder() + ctx, _ := gin.CreateTestContext(w) + ctx.Request = &http.Request{ + Header: make(http.Header), + } + + return ctx, w +} + +func GetGinTestContextWithBody(bodyJsonString string) (c *gin.Context, w *httptest.ResponseRecorder) { + // create a GIN context for test + w = httptest.NewRecorder() + ctx, _ := gin.CreateTestContext(w) + ctx.Request = &http.Request{ + Header: make(http.Header), + Body: io.NopCloser(strings.NewReader(bodyJsonString)), + } + + return ctx, w +} diff --git a/pkg/testutil/gorm_mock.go b/pkg/testutil/gorm_mock.go new file mode 100644 index 0000000..67ef7cd --- /dev/null +++ b/pkg/testutil/gorm_mock.go @@ -0,0 +1,29 @@ +package testutil + +import ( + "log" + + "github.com/DATA-DOG/go-sqlmock" + "gorm.io/driver/postgres" + "gorm.io/gorm" +) + +func GetGORMMock() (*gorm.DB, sqlmock.Sqlmock) { + db, mock, err := sqlmock.New() + if err != nil { + log.Fatalf("an error '%s' was not expected when opening a stub database connection", err) + } + + // create dialector + dialector := postgres.New(postgres.Config{ + Conn: db, + }) + + // open the database + gormdb, err := gorm.Open(dialector, &gorm.Config{SkipDefaultTransaction: true}) + if err != nil { + log.Fatalf("[gorm open] %s", err) + } + + return gormdb, mock +} From 57b22c6c54ed5bcc18d2262c6ae875a23d738e56 Mon Sep 17 00:00:00 2001 From: david Date: Tue, 12 Sep 2023 16:48:01 +0700 Subject: [PATCH 3/7] 04: add integration-test --- Makefile | 12 +++ config/config-local-itest.yml | 9 ++ go.mod | 2 + go.sum | 4 + integration_test/docker-compose.yaml | 13 +++ .../itest_account_create_account_test.go | 65 ++++++++++++++ .../itest_account_get_accounts_test.go | 77 +++++++++++++++++ integration_test/iaccount/main_test.go | 16 ++++ integration_test/init.sql | 8 ++ integration_test/init_data.go | 38 +++++++++ integration_test/setup.go | 85 +++++++++++++++++++ internal/server/http.go | 23 +---- internal/server/server.go | 23 ++++- pkg/testutil/init.go | 11 +++ 14 files changed, 363 insertions(+), 23 deletions(-) create mode 100644 config/config-local-itest.yml create mode 100644 integration_test/docker-compose.yaml create mode 100644 integration_test/iaccount/itest_account_create_account_test.go create mode 100644 integration_test/iaccount/itest_account_get_accounts_test.go create mode 100644 integration_test/iaccount/main_test.go create mode 100644 integration_test/init.sql create mode 100644 integration_test/init_data.go create mode 100644 integration_test/setup.go create mode 100644 pkg/testutil/init.go diff --git a/Makefile b/Makefile index 13a7f15..45e904a 100644 --- a/Makefile +++ b/Makefile @@ -18,4 +18,16 @@ unit-test: @echo "" go test -cover ./internal/... -count=1 +integration-test/db/up: + cp -f ./starter/init.sql ./integration_test/init.sql && \ + docker-compose -f ./integration_test/docker-compose.yaml up -d +integration-test/db/down: + docker-compose -f ./integration_test/docker-compose.yaml down + +integration-test: + @echo "" + @echo "--------- Run integration test ----------" + @echo "" + go test ./integration_test/... -failfast -count=1 + .PHONY: init init/down run mock unit-test diff --git a/config/config-local-itest.yml b/config/config-local-itest.yml new file mode 100644 index 0000000..e803456 --- /dev/null +++ b/config/config-local-itest.yml @@ -0,0 +1,9 @@ +server: + port: ':8000' + name: 'gotemplate' +database: + postgres: + host: 'localhost' + port: 54311 + user: 'template' + dbname: 'templatedb' \ No newline at end of file diff --git a/go.mod b/go.mod index 3553487..0c3bc16 100644 --- a/go.mod +++ b/go.mod @@ -42,6 +42,7 @@ require ( github.com/pelletier/go-toml/v2 v2.0.9 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rogpeppe/go-internal v1.11.0 // indirect + github.com/samber/lo v1.38.1 // indirect github.com/spf13/afero v1.9.5 // indirect github.com/spf13/cast v1.5.1 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect @@ -52,6 +53,7 @@ require ( github.com/ugorji/go/codec v1.2.11 // indirect golang.org/x/arch v0.4.0 // indirect golang.org/x/crypto v0.12.0 // indirect + golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect golang.org/x/net v0.14.0 // indirect golang.org/x/sys v0.11.0 // indirect golang.org/x/text v0.12.0 // indirect diff --git a/go.sum b/go.sum index 6e71686..6618965 100644 --- a/go.sum +++ b/go.sum @@ -203,6 +203,8 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1: github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM= +github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= @@ -266,6 +268,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM= +golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= diff --git a/integration_test/docker-compose.yaml b/integration_test/docker-compose.yaml new file mode 100644 index 0000000..cfa0744 --- /dev/null +++ b/integration_test/docker-compose.yaml @@ -0,0 +1,13 @@ +services: + postgres_itest: + image: postgres:13.3-alpine + restart: always + container_name: postgres_itest + ports: + - "54311:5432" + environment: + - POSTGRES_USER=${POSTGRES_USERNAME} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + - POSTGRES_DB=${POSTGRES_DB} + volumes: + - ./init.sql:/docker-entrypoint-initdb.d/init.sql \ No newline at end of file diff --git a/integration_test/iaccount/itest_account_create_account_test.go b/integration_test/iaccount/itest_account_create_account_test.go new file mode 100644 index 0000000..b0b602a --- /dev/null +++ b/integration_test/iaccount/itest_account_create_account_test.go @@ -0,0 +1,65 @@ +package iaccount + +import ( + "net/http" + "strings" + "testing" + + itest "github.com/gaogao-asia/golang-template/integration_test" + adto "github.com/gaogao-asia/golang-template/internal/account/dto" + "github.com/gaogao-asia/golang-template/internal/domain" + "github.com/gaogao-asia/golang-template/pkg/utils" + "github.com/stretchr/testify/assert" +) + +var ( + createAccountsURLPath = "/api/v1/accounts" +) + +func TestCreateAccounts(t *testing.T) { + tests := []struct { + name string + handler func(t *testing.T) + }{ + { + name: createAccountsURLPath + " success", + handler: createAccountSuccess, + }, + } + + for _, test := range tests { + t.Run(test.name, test.handler) + } +} + +// getUserByPhoneSuccess +func createAccountSuccess(t *testing.T) { + requestBody := strings.NewReader(`{ + "name": "gaogao", + "email": "gaogao@gmail.com", + "roles": ["admin","user"] + }`) + + w, err := itest.CallAPI(http.MethodPost, createAccountsURLPath, requestBody) + assert.NoError(t, err) + + response := w.Result() + statusCode, res, err := itest.ParseResponse(response) + assert.NoError(t, err) + + assert.Equal(t, http.StatusOK, statusCode) + + var dataRes adto.CreateAccountResponse + bytes, err := utils.JsonMarshal(res.Data) + assert.NoError(t, err) + err = utils.JsonUnmarshal(bytes, &dataRes) + assert.NoError(t, err) + + // check account response + assert.Equal(t, "gaogao", dataRes.Account.Name) + assert.Equal(t, "gaogao@gmail.com", dataRes.Account.Email) + assert.Equal(t, []string{"admin", "user"}, dataRes.Account.Roles) + + // Clean data + itest.Conn.DB.Model(&domain.Account{}).Where("id = ?", dataRes.Account.ID).Delete(&domain.Account{}) +} diff --git a/integration_test/iaccount/itest_account_get_accounts_test.go b/integration_test/iaccount/itest_account_get_accounts_test.go new file mode 100644 index 0000000..b79de39 --- /dev/null +++ b/integration_test/iaccount/itest_account_get_accounts_test.go @@ -0,0 +1,77 @@ +package iaccount + +import ( + "net/http" + "testing" + + itest "github.com/gaogao-asia/golang-template/integration_test" + + adto "github.com/gaogao-asia/golang-template/internal/account/dto" + "github.com/gaogao-asia/golang-template/internal/domain" + "github.com/gaogao-asia/golang-template/pkg/utils" + "github.com/samber/lo" + "github.com/stretchr/testify/assert" +) + +var ( + getAccountsURLPath = "/api/v1/accounts" +) + +func TestGetAccounts(t *testing.T) { + tests := []struct { + name string + handler func(t *testing.T) + }{ + { + name: getAccountsURLPath + " success", + handler: getAccountSuccess, + }, + } + + for _, test := range tests { + t.Run(test.name, test.handler) + } +} + +// getUserByPhoneSuccess +func getAccountSuccess(t *testing.T) { + idata := itest.PrepareAccounts(t) + defer idata.Cleanup() + + w, err := itest.CallAPI(http.MethodGet, getAccountsURLPath, nil) + assert.NoError(t, err) + + response := w.Result() + statusCode, res, err := itest.ParseResponse(response) + assert.NoError(t, err) + + assert.Equal(t, http.StatusOK, statusCode) + + var dataRes adto.GetAccountsResponse + bytes, err := utils.JsonMarshal(res.Data) + assert.NoError(t, err) + err = utils.JsonUnmarshal(bytes, &dataRes) + assert.NoError(t, err) + + // check len of accounts + assert.Equal(t, len(idata.Accounts), len(dataRes.Accounts)) + + // convert account response to map + resAccount := make(map[int64]*domain.Account) + lo.Map(idata.Accounts, func(v *domain.Account, i int) int { + resAccount[v.ID] = v + return i + }) + + // check account response + for i, v := range idata.Accounts { + if rv, ok := resAccount[v.ID]; ok { + assert.Equal(t, v.ID, rv.ID) + assert.Equal(t, v.Name, rv.Name) + assert.Equal(t, v.Email, rv.Email) + assert.Equal(t, v.Roles, rv.Roles) + } else { + t.Errorf("account %d not found", i) + } + } +} diff --git a/integration_test/iaccount/main_test.go b/integration_test/iaccount/main_test.go new file mode 100644 index 0000000..811a9a0 --- /dev/null +++ b/integration_test/iaccount/main_test.go @@ -0,0 +1,16 @@ +package iaccount + +import ( + "os" + "testing" + + itest "github.com/gaogao-asia/golang-template/integration_test" +) + +func TestMain(m *testing.M) { + itest.Setup() + + ret := m.Run() + + os.Exit(ret) +} diff --git a/integration_test/init.sql b/integration_test/init.sql new file mode 100644 index 0000000..1bbeaf8 --- /dev/null +++ b/integration_test/init.sql @@ -0,0 +1,8 @@ +create table accounts +( + id BIGSERIAL, + name varchar(255) NOT NULL, + email varchar(255) NOT NULL, + roles varchar(255) NOT NULL, + PRIMARY KEY (id) +) diff --git a/integration_test/init_data.go b/integration_test/init_data.go new file mode 100644 index 0000000..fd343c8 --- /dev/null +++ b/integration_test/init_data.go @@ -0,0 +1,38 @@ +package integrationtest + +import ( + "testing" + + "github.com/gaogao-asia/golang-template/internal/domain" +) + +type ITestData struct { + Accounts []*domain.Account +} + +func PrepareAccounts(t *testing.T) ITestData { + accs := []*domain.Account{ + { + Name: "gaogao", + Email: "gaogao@gmail.com", + Roles: "admin,user", + }, + { + Name: "minh", + Email: "trainer.minhtran@gmail.com", + Roles: "user", + }, + } + err := Conn.DB.Create(&accs).Error + if err != nil { + t.Fatal(err) + } + + return ITestData{Accounts: accs} +} + +func (s *ITestData) Cleanup() { + if len(s.Accounts) > 0 { + Conn.DB.Delete(s.Accounts) + } +} diff --git a/integration_test/setup.go b/integration_test/setup.go new file mode 100644 index 0000000..0724ff7 --- /dev/null +++ b/integration_test/setup.go @@ -0,0 +1,85 @@ +package integrationtest + +import ( + "fmt" + + "io" + "net/http" + "net/http/httptest" + "strings" + + "github.com/gaogao-asia/golang-template/config" + "github.com/gaogao-asia/golang-template/internal/server" + "github.com/gaogao-asia/golang-template/internal/server/http/response" + "github.com/gaogao-asia/golang-template/pkg/connection" + "github.com/gaogao-asia/golang-template/pkg/testutil" + "github.com/gaogao-asia/golang-template/pkg/utils" + "github.com/gin-gonic/gin" +) + +var ( + HardCodePassword = "123456" +) + +var ( + Conn connection.Conn + BaseURL string + Engine *gin.Engine +) + +func Setup() { + testutil.InitConfigForIntegrationTest() + + BaseURL = fmt.Sprintf("http://localhost:%s", config.AppConfig.Server.Port) + + var err error + Conn, err = connection.GetConnection() + if err != nil { + panic(err) + } + + Engine = SetupTestServer(Conn) +} + +func ParseResponse(resp *http.Response) (statusCode int, respData *response.ResponseBody, err error) { + b, err := io.ReadAll(resp.Body) + if err != nil { + return -1, nil, err + } + + res := new(response.ResponseBody) + err = utils.JsonUnmarshal(b, &res) + if err != nil { + return -1, nil, err + } + + return resp.StatusCode, res, nil +} + +func CallAPI(method, apiPath string, body *strings.Reader) (*httptest.ResponseRecorder, error) { + var req *http.Request + var err error + + w := httptest.NewRecorder() + if body != nil { + req, err = http.NewRequest(method, apiPath, body) + } else { + req, err = http.NewRequest(method, apiPath, http.NoBody) + } + if err != nil { + return nil, err + } + req.RequestURI = apiPath + Engine.ServeHTTP(w, req) + + return w, nil +} + +func SetupTestServer(db connection.Conn) *gin.Engine { + gin.SetMode(gin.TestMode) + engine := gin.New() + + api := engine.Group("/api") + server.NewRouter(api, db) + return engine +} diff --git a/internal/server/http.go b/internal/server/http.go index 8a2ff28..9cb7249 100644 --- a/internal/server/http.go +++ b/internal/server/http.go @@ -1,25 +1,15 @@ package server import ( - "log" "net/http" - "github.com/gaogao-asia/golang-template/config" "github.com/gaogao-asia/golang-template/internal/server/http/handler" "github.com/gaogao-asia/golang-template/internal/server/http/middleware" "github.com/gaogao-asia/golang-template/pkg/connection" "github.com/gin-gonic/gin" ) -func newHTTPServer(connection connection.Conn) { - // HTTP Server - engine := gin.New() - engine.RedirectTrailingSlash = false - - engine.GET("/healthz", func(c *gin.Context) { c.Status(http.StatusOK) }) - - api := engine.Group("/api") - +func NewRouter(api *gin.RouterGroup, connection connection.Conn) { // Middleware api.Use(middleware.CustomRecovery()) api.Use(allowAllOrigins()) @@ -27,17 +17,6 @@ func newHTTPServer(connection connection.Conn) { // Routers v1 := handler.NewV1(api, connection) v1.Register() - - // Listen HTTP Server - srv := &http.Server{ - Addr: config.AppConfig.Server.Port, - Handler: engine, - } - - log.Printf("HTTP server listening on %s", srv.Addr) - if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { - log.Fatalf("HTTP server failed to start: %v", err) - } } func allowAllOrigins() gin.HandlerFunc { diff --git a/internal/server/server.go b/internal/server/server.go index d24a3c2..6feb05d 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -1,7 +1,12 @@ package server import ( + "log" + "net/http" + + "github.com/gaogao-asia/golang-template/config" "github.com/gaogao-asia/golang-template/pkg/connection" + "github.com/gin-gonic/gin" ) func Run() { @@ -11,5 +16,21 @@ func Run() { panic(err) } - newHTTPServer(connection) + engine := gin.New() + engine.RedirectTrailingSlash = false + api := engine.Group("/api") + + NewRouter(api, connection) + + // Listen HTTP Server + srv := &http.Server{ + Addr: config.AppConfig.Server.Port, + Handler: engine, + } + + // Start HTTP Server + log.Printf("HTTP server listening on %s", srv.Addr) + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Fatalf("HTTP server failed to start: %v", err) + } } diff --git a/pkg/testutil/init.go b/pkg/testutil/init.go new file mode 100644 index 0000000..977939c --- /dev/null +++ b/pkg/testutil/init.go @@ -0,0 +1,11 @@ +package testutil + +import "github.com/gaogao-asia/golang-template/config" + +func InitConfigForIntegrationTest() { + var err error + config.AppConfig, err = config.ViperLoadConfig("../../config/config-local-itest.yml") + if err != nil { + panic(err) + } +} From 74c8569c1f6b7ef9cf6a081e0d1c74f7083dbcb9 Mon Sep 17 00:00:00 2001 From: david Date: Tue, 12 Sep 2023 16:53:28 +0700 Subject: [PATCH 4/7] 04: remove unuse --- integration_test/setup.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/integration_test/setup.go b/integration_test/setup.go index 0724ff7..9f7c5be 100644 --- a/integration_test/setup.go +++ b/integration_test/setup.go @@ -17,10 +17,6 @@ import ( "github.com/gin-gonic/gin" ) -var ( - HardCodePassword = "123456" -) - var ( Conn connection.Conn BaseURL string From bf2e0c02cb1129f7d3f7f23d7751e47e031c2d2a Mon Sep 17 00:00:00 2001 From: david Date: Wed, 13 Sep 2023 16:25:24 +0700 Subject: [PATCH 5/7] 04: apply wire lib to auto generate di --- Makefile | 6 ++++- README.md | 3 ++- go.mod | 4 ++++ go.sum | 9 ++++++++ .../account/repository/account_reposiroty.go | 2 +- internal/account/service/account_service.go | 2 +- internal/di/account_di.go | 15 ++++++++---- internal/di/init_provider.go | 10 ++++++++ internal/di/wire_gen.go | 23 +++++++++++++++++++ internal/server/http/handler/v1.go | 2 +- pkg/connection/gorm.go | 4 +++- 11 files changed, 69 insertions(+), 11 deletions(-) create mode 100644 internal/di/init_provider.go create mode 100644 internal/di/wire_gen.go diff --git a/Makefile b/Makefile index 45e904a..22aae6d 100644 --- a/Makefile +++ b/Makefile @@ -4,6 +4,7 @@ $(eval export $(shell sed -ne 's/ *#.*$$//; /./ s/=.*$$// p' .env)) init: docker compose -f starter/docker-compose.yaml up -d go install github.com/vektra/mockery/v2@latest + go install github.com/google/wire/cmd/wire@latest init/down: docker compose -f starter/docker-compose.yaml down run: @@ -30,4 +31,7 @@ integration-test: @echo "" go test ./integration_test/... -failfast -count=1 -.PHONY: init init/down run mock unit-test +di: + wire ./internal/di/... + +.PHONY: init init/down run mock unit-test di diff --git a/README.md b/README.md index 5d7e666..ab0152f 100644 --- a/README.md +++ b/README.md @@ -24,4 +24,5 @@ $ make init 2. Run ``` $ make run -``` \ No newline at end of file +``` + diff --git a/go.mod b/go.mod index 0c3bc16..47cd622 100644 --- a/go.mod +++ b/go.mod @@ -24,7 +24,9 @@ require ( github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.15.1 // indirect github.com/goccy/go-json v0.10.2 // indirect + github.com/google/subcommands v1.0.1 // indirect github.com/google/uuid v1.1.2 // indirect + github.com/google/wire v0.5.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect @@ -54,9 +56,11 @@ require ( golang.org/x/arch v0.4.0 // indirect golang.org/x/crypto v0.12.0 // indirect golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect + golang.org/x/mod v0.9.0 // indirect golang.org/x/net v0.14.0 // indirect golang.org/x/sys v0.11.0 // indirect golang.org/x/text v0.12.0 // indirect + golang.org/x/tools v0.6.0 // indirect google.golang.org/protobuf v1.31.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 6618965..f652e64 100644 --- a/go.sum +++ b/go.sum @@ -142,8 +142,12 @@ github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/subcommands v1.0.1 h1:/eqq+otEXm5vhfBrbREPCSVQbvofip6kIz+mX5TUH7k= +github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/wire v0.5.0 h1:I7ELFeVBr3yfPIcc8+MWvrjk+3VjbcSzoXm3JVa+jD8= +github.com/google/wire v0.5.0/go.mod h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1EoU= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= @@ -293,6 +297,8 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs= +golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -406,6 +412,7 @@ golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3 golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190422233926-fe54fb35175b/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= @@ -447,6 +454,8 @@ golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/account/repository/account_reposiroty.go b/internal/account/repository/account_reposiroty.go index dbef915..4b3caad 100644 --- a/internal/account/repository/account_reposiroty.go +++ b/internal/account/repository/account_reposiroty.go @@ -12,7 +12,7 @@ type accountRepository struct { DB *gorm.DB } -func NewAccountRepository(db *gorm.DB) *accountRepository { +func NewAccountRepository(db *gorm.DB) domain.AccountRepository { return &accountRepository{ DB: db, } diff --git a/internal/account/service/account_service.go b/internal/account/service/account_service.go index 2e4e5a9..4f7120d 100644 --- a/internal/account/service/account_service.go +++ b/internal/account/service/account_service.go @@ -10,7 +10,7 @@ type accountService struct { accountRepo domain.AccountRepository } -func NewAccountService(accountRepo domain.AccountRepository) *accountService { +func NewAccountService(accountRepo domain.AccountRepository) domain.AccountService { return &accountService{ accountRepo: accountRepo, } diff --git a/internal/di/account_di.go b/internal/di/account_di.go index f532556..5a7632c 100644 --- a/internal/di/account_di.go +++ b/internal/di/account_di.go @@ -1,14 +1,19 @@ +//go:build wireinject + package di import ( "github.com/gaogao-asia/golang-template/internal/account/handler" accrepo "github.com/gaogao-asia/golang-template/internal/account/repository" accsrv "github.com/gaogao-asia/golang-template/internal/account/service" - "gorm.io/gorm" + "github.com/google/wire" ) -func InitAccountHandler(db *gorm.DB) *handler.AccountHandler { - repo := accrepo.NewAccountRepository(db) - srv := accsrv.NewAccountService(repo) - return handler.NewAccountHandler(srv) +func InitAccountHandler() *handler.AccountHandler { + panic(wire.Build( + initDB, + accrepo.NewAccountRepository, + accsrv.NewAccountService, + handler.NewAccountHandler, + )) } diff --git a/internal/di/init_provider.go b/internal/di/init_provider.go new file mode 100644 index 0000000..68d299b --- /dev/null +++ b/internal/di/init_provider.go @@ -0,0 +1,10 @@ +package di + +import ( + "github.com/gaogao-asia/golang-template/pkg/connection" + "gorm.io/gorm" +) + +func initDB() *gorm.DB { + return connection.DB +} diff --git a/internal/di/wire_gen.go b/internal/di/wire_gen.go new file mode 100644 index 0000000..34345c9 --- /dev/null +++ b/internal/di/wire_gen.go @@ -0,0 +1,23 @@ +// Code generated by Wire. DO NOT EDIT. + +//go:generate go run github.com/google/wire/cmd/wire +//go:build !wireinject +// +build !wireinject + +package di + +import ( + "github.com/gaogao-asia/golang-template/internal/account/handler" + "github.com/gaogao-asia/golang-template/internal/account/repository" + "github.com/gaogao-asia/golang-template/internal/account/service" +) + +// Injectors from account_di.go: + +func InitAccountHandler() *handler.AccountHandler { + db := initDB() + accountRepository := repository.NewAccountRepository(db) + accountService := service.NewAccountService(accountRepository) + accountHandler := handler.NewAccountHandler(accountService) + return accountHandler +} diff --git a/internal/server/http/handler/v1.go b/internal/server/http/handler/v1.go index 47fc402..6afea7e 100644 --- a/internal/server/http/handler/v1.go +++ b/internal/server/http/handler/v1.go @@ -25,7 +25,7 @@ func (r *newRouterParams) Register() { } func (r *newRouterParams) registerAccount() { - accountHandler := di.InitAccountHandler(r.Conn.DB) + accountHandler := di.InitAccountHandler() account := r.v1.Group("/accounts") { diff --git a/pkg/connection/gorm.go b/pkg/connection/gorm.go index ba6338d..5fd934e 100644 --- a/pkg/connection/gorm.go +++ b/pkg/connection/gorm.go @@ -9,6 +9,8 @@ import ( "gorm.io/gorm/schema" ) +var DB *gorm.DB + type Conn struct { DB *gorm.DB } @@ -26,7 +28,7 @@ func GetConnection() (Conn, error) { log.Println(err) return Conn{}, err } - + DB = pgDB return Conn{ DB: pgDB, }, nil From a04e02765b1387401c8816e7adccbfc19530e5b1 Mon Sep 17 00:00:00 2001 From: david Date: Wed, 13 Sep 2023 16:54:52 +0700 Subject: [PATCH 6/7] 04: update wire --- internal/di/{account_di.go => di.go} | 0 internal/di/{init_provider.go => init.go} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename internal/di/{account_di.go => di.go} (100%) rename internal/di/{init_provider.go => init.go} (100%) diff --git a/internal/di/account_di.go b/internal/di/di.go similarity index 100% rename from internal/di/account_di.go rename to internal/di/di.go diff --git a/internal/di/init_provider.go b/internal/di/init.go similarity index 100% rename from internal/di/init_provider.go rename to internal/di/init.go From fcbd6b508ad77714bef86475cd1c2445fdc31e4e Mon Sep 17 00:00:00 2001 From: david Date: Wed, 4 Oct 2023 15:57:12 +0700 Subject: [PATCH 7/7] 04: add product --- internal/domain/product.go | 1 + 1 file changed, 1 insertion(+) create mode 100644 internal/domain/product.go diff --git a/internal/domain/product.go b/internal/domain/product.go new file mode 100644 index 0000000..4188b5a --- /dev/null +++ b/internal/domain/product.go @@ -0,0 +1 @@ +package domain