Skip to content

Latest commit

 

History

History
808 lines (620 loc) · 17.2 KB

notes.md

File metadata and controls

808 lines (620 loc) · 17.2 KB

1 - Create the configs package (config.go, .env)

Cria arquivo de configuração

# root/configs/config.go

import (
	"github.com/go-chi/jwtauth"
	"github.com/spf13/viper"
)

type conf struct {
	DBDriver      string           `mapstructure:"DB_DRIVER"`
	DBHost        string           `mapstructure:"DB_HOST"`
	DBPort        string           `mapstructure:"DB_PORT"`
	DBPassword    string           `mapstrucutre:"DB_PASSWORD"`
	DBName        string           `mapstrucutre:"DB_NAME"`
	WebServerPort string           `mapstrucutre:"WEB_SERVER_PORT"`
	JWTSecret     string           `mapstrucutre:"JWT_SECRET"`
	JWTExpiresIn  int              `mapstrucutre:"JWT_EXPIRES_IN"`
	TokenAuth     *jwtauth.JWTAuth `mapstrucutre:"TOKEN_AUTH"`
}

func LoadConfig(path string) (*conf, error) {

	var cfg *conf
	viper.SetConfigName("app_config")
	viper.SetConfigType("env")
	viper.AddConfigPath(path)
	viper.SetConfigFile(".env")
	viper.AutomaticEnv()

	err := viper.ReadInConfig()
	if err != nil {
		panic(err)
	}

	err = viper.Unmarshal(&cfg)
	if err != nil {
		panic(err)
	}

	cfg.TokenAuth = jwtauth.New("HS256", []byte(cfg.JWTSecret), nil)
	return cfg, nil
}

Carrega as configurações no arquivo main

# root/cmd/server/main.go
package main

import (
	"fmt"

	"github.com/sallescosta/user-and-products-manager/configs"
)

func main() {
	config, _ := configs.LoadConfig(".")
	fmt.Println(config.DBDriver) //só para teste

}

Create entity (user.go)

# root/internal/entity/user.go
package entity

import (
	"log"

	"github.com/sallescosta/user-and-products-manager/pkg/entity"
	"golang.org/x/crypto/bcrypt"
)

type User struct {
	ID       entity.ID `json:"id"`
	Name     string    `json:"name"`
	Email    string    `json:"email"`
	Password string    `json:"-"`
}

func NewUser(name, email, password string) (*User, error) {
	hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)

	if err != nil {
		log.Fatal(err)
		return nil, err
	}

	return &User{
		ID:       entity.NewID(),
		Name:     name,
		Email:    email,
		Password: string(hash),
	}, nil
}

func (u *User) ValidatePassword(password string) bool {
	err := bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(password))
	return err == nil
}

Test entity User

# root/internal/entity
package entity

import (
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestNewUser(t *testing.T) {
	user, err := NewUser("John Doe", "john@gmail.com", "123456")

	assert.Nil(t, err)
	assert.NotNil(t, user)
	assert.NotEmpty(t, user.Password)
	assert.Equal(t, "John Doe", user.Name)
	assert.Equal(t, "john@gmail.com", user.Email)
}

func TestUser_ValidatePassword(t *testing.T) {
	user, err := NewUser("John Doe", "john@gmail.com", "123456")
	assert.Nil(t, err)
	assert.True(t, user.ValidatePassword("123456"))
	assert.False(t, user.ValidatePassword("23456"))
	assert.NotEqual(t, "123456", user.Password)
}


# Para rodar, tem que navegar até a pasta onde esta este arquivo (root/internal/entity) e rodar `go test`

Create Product entity and its test

# root/internal/entity/product.go
package entity

import (
	"errors"
	"time"

	"github.com/sallescosta/user-and-products-manager/pkg/entity"
)

var (
	ErrIDIsRequired    = errors.New("id is required")
	ErrNameIsRequired  = errors.New("name is required")
	ErrPriceIsRequired = errors.New("price is required")
	ErrInvalidPrice    = errors.New("invalid price")
	ErrInvalidId       = errors.New("invalid id")
)

type Product struct {
	ID        entity.ID `json:"id"`
	Name      string    `json:"name"`
	Price     float64   `json:"price"`
	CreatedAt time.Time `json:"created_at"`
}

func (p *Product) Validate() error {
	if p.ID.String() == "" {
		return ErrIDIsRequired
	}

	if _, err := entity.ParseID(p.ID.String()); err != nil {
		return ErrInvalidId
	}
	if p.Name == "" {
		return ErrNameIsRequired
	}
	if p.Price == 0 {
		return ErrPriceIsRequired
	}
	if p.Price < 0 {
		return ErrInvalidPrice
	}
	return nil
}

func NewProduct(name string, price int) (*Product, error) {

	product := &Product{
		ID:        entity.NewID(),
		Name:      name,
		Price:     price,
		CreatedAt: time.Now(),
	}

	err := product.Validate()
	if err != nil {
		return nil, err
	}

	return product, nil

}

product entity unity test

# root/internal/entity/product_test.go
package entity

import (
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestNewProduct(t *testing.T) {
	product, err := NewProduct("book", 15)
	assert.Nil(t, err)
	assert.NotNil(t, product)
	assert.Equal(t, "book", product.Name)
	assert.Equal(t, 15, product.Price)
	assert.NotEmpty(t, product.ID)
	assert.NotEmpty(t, product.CreatedAt)
	assert.Nil(t, product.Validate())
}

func TestProductValidations_Name(t *testing.T) {
	product, err := NewProduct("", 15)
	assert.Nil(t, product)
	assert.Equal(t, err, ErrNameIsRequired)
}

func TestProductValidations_No_Price(t *testing.T) {
	product, err := NewProduct("book", 0)
	assert.Nil(t, product)
	assert.Equal(t, err, ErrPriceIsRequired)
}

func TestProductValidations_Invalid_Price(t *testing.T) {
	product, err := NewProduct("book", -15)
	assert.Nil(t, product)
	assert.Equal(t, err, ErrInvalidPrice)
}

Criação das entities no db

# root/internal/infra/database/interface.go
package database

import "github.com/sallescosta/user-and-products-manager/internal/entity"

type UserInterface interface {
	Create(user *entity.User) error
	FindByEmail(email string) (*entity.User, error)
}

type ProductInterface interface {
	Create(product *entity.Product) error
	FindById(id string) (*entity.Product, error)
	Update(product *entity.Product) error
	Delete(id string) error
	FindAll(page, limit int, sort string) ([]entity.Product, error)
}
# root/internal/infra/database/user_db.go
package database

import (
	"github.com/sallescosta/user-and-products-manager/internal/entity"
	"gorm.io/gorm"
)

type User struct {
	DB *gorm.DB
}

func NewUser(db *gorm.DB) *User {
	return &User{DB: db}
}

func (u *User) Create(user *entity.User) error {
	return u.DB.Create(user).Error
}

func (u *User) FindByEmail(email string) (*entity.User, error) {

	var user entity.User

	err := u.DB.Where("email = ?", email).First(&user).Error
	if err != nil {
		return nil, err
	}

	return &user, nil
}
# root/internal/infra/database/user_db_test.go
package database

import (
	"testing"

	"github.com/sallescosta/user-and-products-manager/internal/entity"
	"github.com/stretchr/testify/assert"
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

func TestCreateUser(t *testing.T) {
	db, err := gorm.Open(sqlite.Open("file::memory:"), &gorm.Config{})
	if err != nil {
		t.Error(err)
	}

	db.AutoMigrate(&entity.User{})
	user, _ := entity.NewUser("John", "john@gmail.com", "123456")

	userDB := NewUser(db)

	err = userDB.Create(user)
	assert.Nil(t, err)

	var userFound entity.User
	err = db.First(&userFound, "id = ?", user.ID).Error
	assert.Nil(t, err)
	assert.Equal(t, userFound.ID, user.ID)
	assert.Equal(t, userFound.Name, user.Name)
	assert.Equal(t, userFound.Email, user.Email)
	assert.NotNil(t, userFound.Password)
}

func TestFindByEmail(t *testing.T) {
	db, err := gorm.Open(sqlite.Open("file::memory:"), &gorm.Config{})
	if err != nil {
		t.Error(err)
	}

	db.AutoMigrate(&entity.User{})
	user, _ := entity.NewUser("John", "john@gmail.com", "123456")

	userDB := NewUser(db)

	err = userDB.Create(user)
	assert.Nil(t, err)

	userFound, err := userDB.FindByEmail(user.Email)
	assert.Nil(t, err)
	assert.Equal(t, userFound.ID, user.ID)
	assert.Equal(t, userFound.Name, user.Name)
	assert.Equal(t, userFound.Email, user.Email)
	assert.NotNil(t, userFound.Password)
}
# root/internal/infra/database/product_db.go
package database

import (
	"github.com/sallescosta/user-and-products-manager/internal/entity"
	"gorm.io/gorm"
)

type Product struct {
	DB *gorm.DB
}

func NewProduct(db *gorm.DB) *Product {
	return &Product{DB: db}
}

func (p *Product) Create(product *entity.Product) error {
	return p.DB.Create(product).Error
}

func (p *Product) FindById(id string) (*entity.Product, error) {
	var product entity.Product

	err := p.DB.Find(&product, "id = ?", id).Error
	if err != nil {
		return nil, err
	}

	return &product, nil
}

func (p *Product) Update(product *entity.Product) error {
	_, err := p.FindById(product.ID.String())
	if err != nil {
		return err
	}

	return p.DB.Save(product).Error
}

func (p *Product) Delete(product *entity.Product) error {
	_, err := p.FindById(product.ID.String())
	if err != nil {
		return err
	}

	return p.DB.Delete(product).Error
}

func (p *Product) FindAll(page, limit int, sort string) ([]entity.Product, error) {
	var products []entity.Product
	var err error
	if sort != "" && sort != "asc" && sort != "desc" {
		sort = "asc"
	}

	if page != 0 && limit != 0 {
		err = p.DB.Limit(limit).Offset((page - 1) * limit).Order("created_at " + sort).Find(&products).Error
	} else {
		err = p.DB.Order("created_at " + sort).Find(&products).Error
	}

	return products, err
}

Integration tests for product_db

# root/internal/infra/database/product_db_test.go

import (
	"fmt"
	"math/rand"
	"testing"

	"github.com/sallescosta/user-and-products-manager/internal/entity"
	"github.com/stretchr/testify/assert"
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

var (
	name    = "Product 1"
	price   = 10.34
	perPage = 10
)

func NewTestDB(t *testing.T) *gorm.DB {
	db, err := gorm.Open(sqlite.Open("file::memory:"), &gorm.Config{})
	if err != nil {
		t.Error(err)
	}

	if err := db.AutoMigrate(&entity.Product{}); err != nil {
		t.Error(err)
	}

	return db
}

func TestDeleteProduct(t *testing.T) {
	db := NewTestDB(t)

	product, err := entity.NewProduct(name, price)
	assert.NoError(t, err)
	db.Create(product)
	productDB := NewProduct(db)

	err = productDB.Delete(product.ID.String())
	assert.NoError(t, err)

	_, err = productDB.FindById(product.ID.String())
	assert.Error(t, err)
}

func TestCreateNewProduct(t *testing.T) {
	db := NewTestDB(t)

	product, err := entity.NewProduct(name, price)
	assert.Nil(t, err)

	productDB := NewProduct(db)

	err = productDB.Create(product)
	assert.NoError(t, err)
	assert.NotEmpty(t, product.ID)
}

func TestFindAllProducts(t *testing.T) {
	db := NewTestDB(t)

	var precoSorteado = rand.Float64() * 100

	for i := 1; i < 33; i++ {
		product, err := entity.NewProduct(fmt.Sprintf("Produto %d", i), precoSorteado)
		assert.NoError(t, err)
		db.Create(product)
	}

	productDB := NewProduct(db)
	products, err := productDB.FindAll(1, perPage, "asc")
	assert.NoError(t, err)

	assert.Len(t, products, perPage)
	assert.Equal(t, "Produto 1", products[0].Name)
	assert.Equal(t, "Produto 10", products[9].Name)

	products, err = productDB.FindAll(2, perPage, "asc")
	assert.NoError(t, err)
	assert.Len(t, products, perPage)
	assert.Equal(t, "Produto 11", products[0].Name)
	assert.Equal(t, "Produto 20", products[9].Name)

	products, err = productDB.FindAll(3, perPage, "asc")
	assert.NoError(t, err)
	assert.Len(t, products, perPage)
	assert.Equal(t, "Produto 21", products[0].Name)
	assert.Equal(t, "Produto 30", products[9].Name)

	products, err = productDB.FindAll(4, perPage, "asc")
	println(products)
	assert.NoError(t, err)
	assert.Len(t, products, 2)
	assert.Equal(t, "Produto 31", products[0].Name)
	assert.Equal(t, "Produto 32", products[1].Name)
}

func TestFindProductByID(t *testing.T) {
	db := NewTestDB(t)

	product, err := entity.NewProduct(name, price)
	assert.NoError(t, err)

	db.Create(product)
	productDB := NewProduct(db)

	product, err = productDB.FindById(product.ID.String())
	assert.NoError(t, err)
	assert.Equal(t, name, product.Name)
	assert.Equal(t, price, product.Price)

}

func TestUpdateProduct(t *testing.T) {
	db := NewTestDB(t)

	product, err := entity.NewProduct(name, price)
	assert.NoError(t, err)

	db.Create(product)
	productDB := NewProduct(db)

	product.Name = "Produto 2"
	err = productDB.Update(product)
	assert.NoError(t, err)

	product, err = productDB.FindById(product.ID.String())
	assert.NoError(t, err)
	assert.Equal(t, "Produto 2", product.Name)
}

Criando Handlers (Produtos)

  • em main.go já deve haver a chamada para o método LoadConfig e a criação do banco de dados (com ORM fazendo a migração das entities):
func main() {
	config, _ := configs.LoadConfig(".")

	db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
	if err != nil {
		panic(err)
	}

	db.AutoMigrate(&entity.Product{}, &entity.User{})
}

Criação de DTOs (Não tem dependencia nenhuma..)

# root/internal/dto/dto.go

package dto

type CreateProductInput struct {
	Name  string  `json:"name"`
	Price float64 `json:"price"`
}

type CreateUserInput struct {
	Name     string `json:"name"`
	Email    string `json:"email"`
	Password string `json:"password"`
}

type GetJWTInput struct {
	Email    string `json:"email"`
	Password string `json:"password"`
}

type GetJWTOutput struct {
	AccessToken string `json:"access_token"`
}

type GetUsersOutput struct {
	Name  string `json:"name"`
	Email string `json:"email"`
}
# root/internal/infra/webserver/handlers/product_handlers.go
type ProductHandler struct {
	ProductDB database.ProductInterface
}

func NewProductHandler(db database.ProductInterface) *ProductHandler {
	return &ProductHandler{
		ProductDB: db,
	}
}

func (h *ProductHandler) CreateProduct(w http.ResponseWriter, r *http.Request) {
	var product dto.CreateProductInput

	err := json.NewDecoder(r.Body).Decode(&product)
	if err != nil {
		w.WriteHeader(http.StatusBadRequest)
		return
	}

	p, err := entity.NewProduct(product.Name, product.Price)  // isso normalmente não é bom. Não é normal que o handler saiba como criar uma entidade.
	if err != nil {
		w.WriteHeader(http.StatusBadRequest)
		return
	}

	err = h.ProductDB.Create(p)
	if err != nil {
		w.WriteHeader(http.StatusInternalServerError)
	}

	w.WriteHeader(http.StatusCreated)
}
// para testar, independente de roteador ou framework, é só usar um:
// http.handleFunc("/products", h.CreateProduct)
// http.ListenAndServe(":8000", nil)
// e chmar num POST
// para verificar no db (sqlite) se está ok, abre um outro terminal
// e roda `sqlite3 cmd/server/test.db` e depois `select * from products;`

Implementação do roteador (Chi)

  • com o roteador e os handlers, o código do main.go fica assim:
func main() {
	config, _ := configs.LoadConfig(".")

	db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
	if err != nil {
		panic(err)
	}

	db.AutoMigrate(&entity.Product{}, &entity.User{})

	productHandler := handlers.NewProductHandler(database.NewProduct(db))
	userHandler := handlers.NewUserHandler(database.NewUser(db))

	r := chi.NewRouter()
	r.Use(middleware.Logger)
	r.Use(middleware.Recoverer)
	r.Use(middleware.WithValue("jwt", config.TokenAuth))
	r.Use(middleware.WithValue("JwtExpiresIn", config.JWTExpiresIn))

	r.Use(LogRequest)

	r.Route("/products", func(r chi.Router) {
		r.Use(jwtauth.Verifier(config.TokenAuth))
		r.Use(jwtauth.Authenticator)

		r.Get("/", productHandler.GetProducts)
		r.Post("/", productHandler.CreateProduct)
		r.Get("/{id}", productHandler.GetProduct)
		r.Put("/{id}", productHandler.UpdateProduct)
		r.Delete("/{id}", productHandler.DeleteProduct)
	})

	r.Post("/users", userHandler.CreateUser)
	r.Post("/users/generate_token", userHandler.GetJWT)
	r.Get("/users", userHandler.AllUsers)

	r.Route("/users", func(r chi.Router) {
		r.Use(jwtauth.Verifier(config.TokenAuth))
		r.Use(jwtauth.Authenticator)
	})

	http.HandleFunc("/products", productHandler.CreateProduct)

	r.Get("/docs/*", httpSwagger.Handler(httpSwagger.URL("http://localhost:8000/docs/doc.json")))
	log.Fatal(http.ListenAndServe(":8000", r))
}

Gerando JWT

  • os jwt estão associados aos usuários, então deve ter jwt na interface do usuário
# root/internal/infra/webserver/handlers/user_handlers.go

import "github.com/go-chi/jwtauth"

type UserHandler struct {
	UserDB       database.UserInterface
	Jwt          *jwtauth.JWTAuth
	JwtExpiresIn int
}
  • Package publico de geração de JWT
# root/pkg/jwt/jwt.go
package entity

import (
	"github.com/google/uuid"
)

type ID = uuid.UUID

func NewID() ID {
	return ID(uuid.New())
}

func ParseID(s string) (ID, error) {
	id, err := uuid.Parse(s)
	return ID(id), err
}
  • Criar o handler GetJwt // tem que ter o dto pronto (dto.GetJWTInput) //
# root/internal/infra/webserver/handlers/user_handlers.go
func (h *UserHandler) GetJWT(w http.ResponseWriter, r *http.Request) {
	jwt := r.Context().Value("jwt").(*jwtauth.JWTAuth)
	jwtExpiresIn := r.Context().Value("JwtExpiresIn").(int)

// serializa o body
	var user dto.GetJWTInput
	if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
		w.WriteHeader(http.StatusBadRequest)
		return
	}

	// verifica se o usuário existe
	u, err := h.UserDB.FindByEmail(user.Email)
	if err != nil {
		w.WriteHeader(http.StatusNotFound)
		err := Error{Message: err.Error()}
		json.NewEncoder(w).Encode(err)
	}

	// se chegou aqui, o usuário existe, então verifica a senha
	if !u.ValidatePassword(user.Password) {
		w.WriteHeader(http.StatusUnauthorized)
		return
	}

	m := map[string]interface{}{
		"sub": u.ID.String(),
		"exp": time.Now().Add(time.Second * time.Duration(jwtExpiresIn)).Unix(),
	}
	_, tokenString, _ := jwt.Encode(m)

	accessToken := dto.GetJWTOutput{AccessToken: tokenString}

	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusOK)
	err = json.NewEncoder(w).Encode(accessToken)
	if err != nil {
		return
	}
}