mirror of
https://github.com/3ybactuk/marketplace-go-service-project.git
synced 2025-10-30 05:53:45 +03:00
[hw-3] loms service
This commit is contained in:
@@ -1,19 +1,21 @@
|
||||
FROM golang:1.23.1-alpine as builder
|
||||
FROM golang:1.23.9-alpine as builder
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
COPY go.mod go.mod
|
||||
COPY go.sum go.sum
|
||||
COPY cart/go.mod go.mod
|
||||
COPY cart/go.sum go.sum
|
||||
|
||||
RUN go mod download
|
||||
|
||||
COPY . .
|
||||
|
||||
WORKDIR cart
|
||||
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -o /server ./cmd/server/main.go
|
||||
|
||||
FROM scratch
|
||||
COPY --from=builder server /bin/server
|
||||
COPY configs/values_local.yaml /bin/config/values_local.yaml
|
||||
COPY cart/configs/values_local.yaml /bin/config/values_local.yaml
|
||||
|
||||
ENV CONFIG_FILE=/bin/config/values_local.yaml
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ go 1.23.9
|
||||
require (
|
||||
github.com/rs/zerolog v1.34.0
|
||||
github.com/stretchr/testify v1.10.0
|
||||
google.golang.org/grpc v1.72.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
|
||||
@@ -54,6 +54,8 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/gojuno/minimock/v3 v3.4.5 h1:Jcb0tEYZvVlQNtAAYpg3jCOoSwss2c1/rNugYTzj304=
|
||||
github.com/gojuno/minimock/v3 v3.4.5/go.mod h1:o9F8i2IT8v3yirA7mmdpNGzh1WNesm6iQakMtQV6KiE=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
@@ -147,6 +149,8 @@ go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCRE
|
||||
go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
|
||||
go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
|
||||
go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
|
||||
go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
|
||||
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
|
||||
go.opentelemetry.io/proto/otlp v1.6.0 h1:jQjP+AQyTf+Fe7OKj/MfkDrmK4MNVtw2NpXsf9fefDI=
|
||||
|
||||
@@ -9,14 +9,19 @@ import (
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
|
||||
"route256/cart/internal/app/server"
|
||||
"route256/cart/internal/domain/cart/repository"
|
||||
"route256/cart/internal/domain/cart/service"
|
||||
product_service "route256/cart/internal/domain/products/service"
|
||||
loms_service "route256/cart/internal/clients/loms"
|
||||
product_service "route256/cart/internal/clients/products"
|
||||
"route256/cart/internal/domain/repository"
|
||||
"route256/cart/internal/domain/service"
|
||||
"route256/cart/internal/infra/config"
|
||||
"route256/cart/internal/infra/http/middlewares"
|
||||
"route256/cart/internal/infra/http/round_trippers"
|
||||
|
||||
pbLOMS "route256/pkg/api/loms/v1"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -39,9 +44,9 @@ func NewApp(configPath string) (*App, error) {
|
||||
zerolog.SetGlobalLevel(zerolog.InfoLevel)
|
||||
|
||||
if c.Service.LogLevel != "" {
|
||||
level, err := zerolog.ParseLevel(c.Service.LogLevel)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unknown log level `%s` provided: %w", c.Service.LogLevel, err)
|
||||
level, logErr := zerolog.ParseLevel(c.Service.LogLevel)
|
||||
if logErr != nil {
|
||||
return nil, fmt.Errorf("unknown log level `%s` provided: %w", c.Service.LogLevel, logErr)
|
||||
}
|
||||
|
||||
zerolog.SetGlobalLevel(level)
|
||||
@@ -52,7 +57,13 @@ func NewApp(configPath string) (*App, error) {
|
||||
app := &App{
|
||||
config: c,
|
||||
}
|
||||
app.server.Handler = app.BootstrapHandlers(app.setupCartService())
|
||||
|
||||
service, err := app.setupCartService()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
app.server.Handler = app.BootstrapHandlers(service)
|
||||
|
||||
return app, nil
|
||||
}
|
||||
@@ -70,7 +81,8 @@ func (app *App) ListenAndServe() error {
|
||||
return app.server.Serve(l)
|
||||
}
|
||||
|
||||
func (app *App) setupCartService() *service.CartService {
|
||||
func (app *App) setupCartService() (*service.CartService, error) {
|
||||
// Product service client
|
||||
transport := http.DefaultTransport
|
||||
transport = round_trippers.NewLogRoundTripper(transport)
|
||||
transport = round_trippers.NewRetryRoundTripper(transport, productsRetryAttemptsDefault, productsInitialDelaySecDefault)
|
||||
@@ -86,10 +98,21 @@ func (app *App) setupCartService() *service.CartService {
|
||||
fmt.Sprintf("%s:%s", app.config.ProductService.Host, app.config.ProductService.Port),
|
||||
)
|
||||
|
||||
// LOMS service client
|
||||
conn, err := grpc.NewClient(
|
||||
fmt.Sprintf("%s:%s", app.config.LomsService.Host, app.config.LomsService.Port),
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("grpc.NewClient: %w", err)
|
||||
}
|
||||
|
||||
lomsClient := pbLOMS.NewLOMSClient(conn)
|
||||
lomsService := loms_service.NewLomsService(lomsClient)
|
||||
|
||||
const userCartCap = 100
|
||||
cartRepository := repository.NewInMemoryRepository(userCartCap)
|
||||
|
||||
return service.NewCartService(cartRepository, productService)
|
||||
return service.NewCartService(cartRepository, productService, lomsService), nil
|
||||
}
|
||||
|
||||
func (app *App) BootstrapHandlers(cartService *service.CartService) http.Handler {
|
||||
@@ -97,6 +120,7 @@ func (app *App) BootstrapHandlers(cartService *service.CartService) http.Handler
|
||||
|
||||
mx := http.NewServeMux()
|
||||
mx.HandleFunc("POST /user/{user_id}/cart/{sku_id}", s.AddItemHandler)
|
||||
mx.HandleFunc("POST /checkout/{user_id}", s.CheckoutHandler)
|
||||
mx.HandleFunc("GET /user/{user_id}/cart", s.GetItemsByUserIDHandler)
|
||||
mx.HandleFunc("DELETE /user/{user_id}/cart/{sku_id}", s.DeleteItemHandler)
|
||||
mx.HandleFunc("DELETE /user/{user_id}/cart", s.DeleteItemsByUserIDHandler)
|
||||
|
||||
@@ -13,12 +13,12 @@ import (
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type CreateReviewRequest struct {
|
||||
type AddItemRequest struct {
|
||||
Count uint32 `json:"count" validate:"required,gt=0"`
|
||||
}
|
||||
|
||||
func (s *Server) AddItemHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var request CreateReviewRequest
|
||||
var request AddItemRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
|
||||
makeErrorResponse(w, err, http.StatusBadRequest)
|
||||
|
||||
@@ -79,7 +79,7 @@ func (s *Server) AddItemHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if err := s.cartService.AddItem(r.Context(), uid, item); err != nil {
|
||||
switch {
|
||||
case errors.Is(err, model.ErrProductNotFound):
|
||||
case errors.Is(err, model.ErrProductNotFound), errors.Is(err, model.ErrNotEnoughStocks):
|
||||
makeErrorResponse(w, err, http.StatusPreconditionFailed)
|
||||
|
||||
log.Trace().Err(err).Msgf("product does not exist")
|
||||
|
||||
58
cart/internal/app/server/checkout_handler.go
Normal file
58
cart/internal/app/server/checkout_handler.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"route256/cart/internal/domain/entity"
|
||||
"route256/cart/internal/domain/model"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type CheckoutResponse struct {
|
||||
OrderID int64 `json:"order_id"`
|
||||
}
|
||||
|
||||
func (s *Server) CheckoutHandler(w http.ResponseWriter, r *http.Request) {
|
||||
strUserID := r.PathValue("user_id")
|
||||
userID, err := strconv.ParseInt(strUserID, 10, 64)
|
||||
if err != nil || userID <= 0 {
|
||||
if err == nil {
|
||||
err = fmt.Errorf("user_id must be greater than 0")
|
||||
}
|
||||
|
||||
makeErrorResponse(w, err, http.StatusBadRequest)
|
||||
|
||||
log.Trace().Err(err).Msgf("user_id=`%s`", strUserID)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
orderID, err := s.cartService.CheckoutUserCart(r.Context(), entity.UID(userID))
|
||||
switch {
|
||||
case errors.Is(err, model.ErrCartNotFound):
|
||||
makeErrorResponse(w, err, http.StatusNotFound)
|
||||
|
||||
return
|
||||
case err != nil:
|
||||
makeErrorResponse(w, err, http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
resp := CheckoutResponse{
|
||||
OrderID: orderID,
|
||||
}
|
||||
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
makeErrorResponse(w, err, http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ type CartService interface {
|
||||
GetItemsByUserID(ctx context.Context, userID entity.UID) (*model.Cart, error)
|
||||
DeleteItem(ctx context.Context, userID entity.UID, sku entity.Sku) error
|
||||
DeleteItemsByUserID(ctx context.Context, userID entity.UID) error
|
||||
CheckoutUserCart(ctx context.Context, userID entity.UID) (int64, error)
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
|
||||
56
cart/internal/clients/loms/service.go
Normal file
56
cart/internal/clients/loms/service.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package loms
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"route256/cart/internal/domain/entity"
|
||||
"route256/cart/internal/domain/model"
|
||||
|
||||
pbLoms "route256/pkg/api/loms/v1"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
grpcClient pbLoms.LOMSClient
|
||||
}
|
||||
|
||||
func NewLomsService(grpcClient pbLoms.LOMSClient) *Service {
|
||||
return &Service{
|
||||
grpcClient: grpcClient,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) OrderCreate(ctx context.Context, cart *model.Cart) (int64, error) {
|
||||
items := make([]*pbLoms.OrderItem, len(cart.Items))
|
||||
for i, item := range cart.Items {
|
||||
items[i] = &pbLoms.OrderItem{
|
||||
Sku: int64(item.Product.Sku),
|
||||
Count: item.Count,
|
||||
}
|
||||
}
|
||||
|
||||
req := &pbLoms.OrderCreateRequest{
|
||||
UserId: int64(cart.UserID),
|
||||
Items: items,
|
||||
}
|
||||
|
||||
resp, err := s.grpcClient.OrderCreate(ctx, req)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("grpcClient.OrderCreate: %w", err)
|
||||
}
|
||||
|
||||
return resp.OrderId, nil
|
||||
}
|
||||
|
||||
func (s *Service) StocksInfo(ctx context.Context, sku entity.Sku) (uint32, error) {
|
||||
req := &pbLoms.StocksInfoRequest{
|
||||
Sku: int64(sku),
|
||||
}
|
||||
|
||||
resp, err := s.grpcClient.StocksInfo(ctx, req)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("grpcClient.StocksInfo: %w", err)
|
||||
}
|
||||
|
||||
return resp.Count, nil
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package products
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,7 +1,7 @@
|
||||
package entity
|
||||
|
||||
type (
|
||||
UID uint64
|
||||
UID int64
|
||||
Sku int64
|
||||
)
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import "errors"
|
||||
|
||||
var (
|
||||
ErrProductNotFound = errors.New("invalid sku")
|
||||
ErrNotEnoughStocks = errors.New("not enough stocks")
|
||||
|
||||
ErrCartNotFound = errors.New("cart not found")
|
||||
ErrItemNotFoundInCart = errors.New("item not found in cart")
|
||||
|
||||
@@ -6,10 +6,10 @@ import (
|
||||
"slices"
|
||||
"sync"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"route256/cart/internal/domain/entity"
|
||||
"route256/cart/internal/domain/model"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
//go:generate minimock -i Repository -o ./mock -s _mock.go
|
||||
@@ -24,15 +24,23 @@ type ProductService interface {
|
||||
GetProductBySku(ctx context.Context, sku entity.Sku) (*model.Product, error)
|
||||
}
|
||||
|
||||
type CartService struct {
|
||||
repository Repository
|
||||
productService ProductService
|
||||
type LomsService interface {
|
||||
OrderCreate(ctx context.Context, cart *model.Cart) (int64, error)
|
||||
StocksInfo(ctx context.Context, sku entity.Sku) (uint32, error)
|
||||
}
|
||||
|
||||
func NewCartService(repository Repository, productService ProductService) *CartService {
|
||||
type CartService struct {
|
||||
repository Repository
|
||||
|
||||
productService ProductService
|
||||
lomsService LomsService
|
||||
}
|
||||
|
||||
func NewCartService(repository Repository, productService ProductService, lomsService LomsService) *CartService {
|
||||
return &CartService{
|
||||
repository: repository,
|
||||
productService: productService,
|
||||
lomsService: lomsService,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +58,15 @@ func (s *CartService) AddItem(ctx context.Context, userID entity.UID, item *mode
|
||||
return fmt.Errorf("productService.GetProductBySku: %w", err)
|
||||
}
|
||||
|
||||
count, err := s.lomsService.StocksInfo(ctx, item.Product.Sku)
|
||||
if err != nil {
|
||||
return fmt.Errorf("lomsService.StocksInfo: %w", err)
|
||||
}
|
||||
|
||||
if count < item.Count {
|
||||
return model.ErrNotEnoughStocks
|
||||
}
|
||||
|
||||
if err := s.repository.AddItem(ctx, userID, item); err != nil {
|
||||
return fmt.Errorf("repository.AddItemToCart: %w", err)
|
||||
}
|
||||
@@ -170,3 +187,25 @@ func (s *CartService) DeleteItemsByUserID(ctx context.Context, userID entity.UID
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *CartService) CheckoutUserCart(ctx context.Context, userID entity.UID) (int64, error) {
|
||||
if userID <= 0 {
|
||||
return 0, fmt.Errorf("userID invalid")
|
||||
}
|
||||
|
||||
cart, err := s.GetItemsByUserID(ctx, entity.UID(userID))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
orderID, err := s.lomsService.OrderCreate(ctx, cart)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("lomsService.OrderCreate: %w", err)
|
||||
}
|
||||
|
||||
if err := s.DeleteItemsByUserID(ctx, userID); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return orderID, nil
|
||||
}
|
||||
@@ -9,9 +9,9 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"route256/cart/internal/domain/cart/service/mock"
|
||||
"route256/cart/internal/domain/entity"
|
||||
"route256/cart/internal/domain/model"
|
||||
"route256/cart/internal/domain/service/mock"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -19,9 +19,9 @@ const (
|
||||
validName = "some product name"
|
||||
)
|
||||
|
||||
type ProductServiceFake struct{}
|
||||
type productServiceFake struct{}
|
||||
|
||||
func (f *ProductServiceFake) GetProductBySku(_ context.Context, sku entity.Sku) (*model.Product, error) {
|
||||
func (f *productServiceFake) GetProductBySku(_ context.Context, sku entity.Sku) (*model.Product, error) {
|
||||
if sku%2 == 0 {
|
||||
return nil, errors.New("empty shelf")
|
||||
}
|
||||
@@ -33,6 +33,24 @@ func (f *ProductServiceFake) GetProductBySku(_ context.Context, sku entity.Sku)
|
||||
}, nil
|
||||
}
|
||||
|
||||
type lomsServiceFake struct{}
|
||||
|
||||
func (f *lomsServiceFake) StocksInfo(_ context.Context, sku entity.Sku) (uint32, error) {
|
||||
if sku == 1111 {
|
||||
return 0, errors.New("stock error")
|
||||
}
|
||||
|
||||
return 100, nil
|
||||
}
|
||||
|
||||
func (f *lomsServiceFake) OrderCreate(_ context.Context, cart *model.Cart) (int64, error) {
|
||||
if cart.UserID == 1111 {
|
||||
return 0, errors.New("order create error")
|
||||
}
|
||||
|
||||
return 1234, nil
|
||||
}
|
||||
|
||||
func TestCartService_AddItem(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -49,9 +67,20 @@ func TestCartService_AddItem(t *testing.T) {
|
||||
Count: 1,
|
||||
}
|
||||
|
||||
testSKULomsFailing := entity.Sku(1111)
|
||||
testItemLomsFailing := model.Item{
|
||||
Product: &model.Product{
|
||||
Name: validName,
|
||||
Price: validPrice,
|
||||
Sku: testSKULomsFailing,
|
||||
},
|
||||
Count: 1,
|
||||
}
|
||||
|
||||
type fields struct {
|
||||
repository Repository
|
||||
productService ProductService
|
||||
lomsService LomsService
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
@@ -72,7 +101,8 @@ func TestCartService_AddItem(t *testing.T) {
|
||||
AddItemMock.
|
||||
Expect(ctx, 1337, &testItem).
|
||||
Return(nil),
|
||||
productService: &ProductServiceFake{},
|
||||
productService: &productServiceFake{},
|
||||
lomsService: &lomsServiceFake{},
|
||||
},
|
||||
args: args{
|
||||
ctx: ctx,
|
||||
@@ -118,7 +148,8 @@ func TestCartService_AddItem(t *testing.T) {
|
||||
name: "product service error",
|
||||
fields: fields{
|
||||
repository: nil,
|
||||
productService: &ProductServiceFake{},
|
||||
productService: &productServiceFake{},
|
||||
lomsService: &lomsServiceFake{},
|
||||
},
|
||||
args: args{
|
||||
ctx: ctx,
|
||||
@@ -141,7 +172,8 @@ func TestCartService_AddItem(t *testing.T) {
|
||||
AddItemMock.
|
||||
Expect(ctx, 1337, &testItem).
|
||||
Return(assert.AnError),
|
||||
productService: &ProductServiceFake{},
|
||||
productService: &productServiceFake{},
|
||||
lomsService: &lomsServiceFake{},
|
||||
},
|
||||
args: args{
|
||||
ctx: ctx,
|
||||
@@ -150,6 +182,41 @@ func TestCartService_AddItem(t *testing.T) {
|
||||
},
|
||||
wantErr: require.Error,
|
||||
},
|
||||
{
|
||||
name: "stocks acquiring error",
|
||||
fields: fields{
|
||||
repository: nil,
|
||||
productService: &productServiceFake{},
|
||||
lomsService: &lomsServiceFake{},
|
||||
},
|
||||
args: args{
|
||||
ctx: ctx,
|
||||
item: &testItemLomsFailing,
|
||||
userID: 1337,
|
||||
},
|
||||
wantErr: require.Error,
|
||||
},
|
||||
{
|
||||
name: "not enough stocks",
|
||||
fields: fields{
|
||||
repository: nil,
|
||||
productService: &productServiceFake{},
|
||||
lomsService: &lomsServiceFake{},
|
||||
},
|
||||
args: args{
|
||||
ctx: ctx,
|
||||
item: &model.Item{
|
||||
Product: &model.Product{
|
||||
Name: validName,
|
||||
Price: validPrice,
|
||||
Sku: testSKU,
|
||||
},
|
||||
Count: 10000,
|
||||
},
|
||||
userID: 1337,
|
||||
},
|
||||
wantErr: require.Error,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
@@ -158,6 +225,7 @@ func TestCartService_AddItem(t *testing.T) {
|
||||
s := &CartService{
|
||||
repository: tt.fields.repository,
|
||||
productService: tt.fields.productService,
|
||||
lomsService: tt.fields.lomsService,
|
||||
}
|
||||
|
||||
err := s.AddItem(tt.args.ctx, tt.args.userID, tt.args.item)
|
||||
@@ -218,7 +286,7 @@ func TestCartService_GetItemsByUserID(t *testing.T) {
|
||||
GetItemsByUserIDMock.
|
||||
Expect(ctx, testUID).
|
||||
Return(testEntityCart, nil),
|
||||
productService: &ProductServiceFake{},
|
||||
productService: &productServiceFake{},
|
||||
},
|
||||
args: args{
|
||||
ctx: ctx,
|
||||
@@ -238,7 +306,7 @@ func TestCartService_GetItemsByUserID(t *testing.T) {
|
||||
Items: []entity.Sku{testSKU},
|
||||
ItemCount: map[entity.Sku]uint32{testSKU: 2},
|
||||
}, nil),
|
||||
productService: &ProductServiceFake{},
|
||||
productService: &productServiceFake{},
|
||||
},
|
||||
args: args{
|
||||
ctx: ctx,
|
||||
@@ -271,7 +339,7 @@ func TestCartService_GetItemsByUserID(t *testing.T) {
|
||||
Items: []entity.Sku{testSKU, 1},
|
||||
ItemCount: map[entity.Sku]uint32{testSKU: 1, 1: 1},
|
||||
}, nil),
|
||||
productService: &ProductServiceFake{},
|
||||
productService: &productServiceFake{},
|
||||
},
|
||||
args: args{
|
||||
ctx: ctx,
|
||||
@@ -357,7 +425,7 @@ func TestCartService_GetItemsByUserID(t *testing.T) {
|
||||
Items: []entity.Sku{2},
|
||||
ItemCount: map[entity.Sku]uint32{2: 1},
|
||||
}, nil),
|
||||
productService: &ProductServiceFake{},
|
||||
productService: &productServiceFake{},
|
||||
},
|
||||
args: args{
|
||||
ctx: ctx,
|
||||
@@ -377,7 +445,7 @@ func TestCartService_GetItemsByUserID(t *testing.T) {
|
||||
Items: []entity.Sku{2},
|
||||
ItemCount: map[entity.Sku]uint32{2: 1},
|
||||
}, nil),
|
||||
productService: &ProductServiceFake{},
|
||||
productService: &productServiceFake{},
|
||||
},
|
||||
args: args{
|
||||
ctx: ctx,
|
||||
@@ -573,3 +641,118 @@ func TestCartService_DeleteItemsByUserID(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCartService_CheckoutUserCart(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := context.Background()
|
||||
mc := minimock.NewController(t)
|
||||
|
||||
testSKU := entity.Sku(199)
|
||||
testCart := entity.Cart{
|
||||
Items: []entity.Sku{testSKU},
|
||||
ItemCount: map[entity.Sku]uint32{testSKU: 1},
|
||||
}
|
||||
|
||||
type fields struct {
|
||||
repository Repository
|
||||
productService ProductService
|
||||
lomsService LomsService
|
||||
}
|
||||
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
userID entity.UID
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wantOrderID int64
|
||||
wantErr require.ErrorAssertionFunc
|
||||
}{
|
||||
{
|
||||
name: "success",
|
||||
fields: fields{
|
||||
repository: mock.NewRepositoryMock(mc).
|
||||
GetItemsByUserIDMock.
|
||||
Expect(ctx, 1337).
|
||||
Return(testCart, nil).
|
||||
DeleteItemsByUserIDMock.
|
||||
Expect(ctx, 1337).
|
||||
Return(nil),
|
||||
productService: &productServiceFake{},
|
||||
lomsService: &lomsServiceFake{},
|
||||
},
|
||||
args: args{ctx: ctx, userID: 1337},
|
||||
wantOrderID: 1234,
|
||||
wantErr: require.NoError,
|
||||
},
|
||||
{
|
||||
name: "invalid user id",
|
||||
fields: fields{},
|
||||
args: args{ctx: ctx, userID: 0},
|
||||
wantErr: require.Error,
|
||||
},
|
||||
{
|
||||
name: "get cart error",
|
||||
fields: fields{
|
||||
repository: mock.NewRepositoryMock(mc).
|
||||
GetItemsByUserIDMock.
|
||||
Expect(ctx, 1337).
|
||||
Return(entity.Cart{}, assert.AnError),
|
||||
},
|
||||
args: args{ctx: ctx, userID: 1337},
|
||||
wantErr: require.Error,
|
||||
},
|
||||
{
|
||||
name: "order create error",
|
||||
fields: fields{
|
||||
repository: mock.NewRepositoryMock(mc).
|
||||
GetItemsByUserIDMock.
|
||||
Expect(ctx, 1111).
|
||||
Return(testCart, nil),
|
||||
productService: &productServiceFake{},
|
||||
lomsService: &lomsServiceFake{},
|
||||
},
|
||||
args: args{ctx: ctx, userID: 1111},
|
||||
wantErr: require.Error,
|
||||
},
|
||||
{
|
||||
name: "delete order error",
|
||||
fields: fields{
|
||||
repository: mock.NewRepositoryMock(mc).
|
||||
GetItemsByUserIDMock.
|
||||
Expect(ctx, 1337).
|
||||
Return(testCart, nil).
|
||||
DeleteItemsByUserIDMock.
|
||||
Expect(ctx, 1337).
|
||||
Return(assert.AnError),
|
||||
productService: &productServiceFake{},
|
||||
lomsService: &lomsServiceFake{},
|
||||
},
|
||||
args: args{ctx: ctx, userID: 1337},
|
||||
wantErr: require.Error,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
svc := &CartService{
|
||||
repository: tt.fields.repository,
|
||||
productService: tt.fields.productService,
|
||||
lomsService: tt.fields.lomsService,
|
||||
}
|
||||
|
||||
orderID, err := svc.CheckoutUserCart(tt.args.ctx, tt.args.userID)
|
||||
tt.wantErr(t, err)
|
||||
if err == nil {
|
||||
require.Equal(t, tt.wantOrderID, orderID)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
@@ -16,13 +17,17 @@ import (
|
||||
"github.com/ozontech/allure-go/pkg/framework/suite"
|
||||
"github.com/testcontainers/testcontainers-go"
|
||||
"github.com/testcontainers/testcontainers-go/wait"
|
||||
"google.golang.org/grpc"
|
||||
|
||||
lomsService "route256/cart/internal/adapter/client/loms"
|
||||
productsService "route256/cart/internal/adapter/client/products"
|
||||
"route256/cart/internal/app"
|
||||
"route256/cart/internal/domain/cart/repository"
|
||||
cartService "route256/cart/internal/domain/cart/service"
|
||||
"route256/cart/internal/domain/entity"
|
||||
"route256/cart/internal/domain/model"
|
||||
productsService "route256/cart/internal/domain/products/service"
|
||||
"route256/cart/internal/domain/repository"
|
||||
cartService "route256/cart/internal/domain/service"
|
||||
|
||||
pbLoms "route256/pkg/api/loms/v1"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -34,12 +39,27 @@ const (
|
||||
testUID = entity.UID(1337)
|
||||
)
|
||||
|
||||
type lomsServerMock struct {
|
||||
pbLoms.UnimplementedLOMSServer
|
||||
}
|
||||
|
||||
func (lomsServerMock) OrderCreate(_ context.Context, _ *pbLoms.OrderCreateRequest) (*pbLoms.OrderCreateResponse, error) {
|
||||
return &pbLoms.OrderCreateResponse{OrderId: 1}, nil
|
||||
}
|
||||
|
||||
func (lomsServerMock) StocksInfo(_ context.Context, _ *pbLoms.StocksInfoRequest) (*pbLoms.StocksInfoResponse, error) {
|
||||
return &pbLoms.StocksInfoResponse{Count: 1000}, nil
|
||||
}
|
||||
|
||||
type CartHandlerSuite struct {
|
||||
suite.Suite
|
||||
|
||||
psContainer testcontainers.Container
|
||||
server *httptest.Server
|
||||
|
||||
lomsSrv *grpc.Server
|
||||
lomsConn *grpc.ClientConn
|
||||
|
||||
server *httptest.Server
|
||||
cartSvc *cartService.CartService
|
||||
}
|
||||
|
||||
@@ -72,6 +92,20 @@ func (s *CartHandlerSuite) BeforeAll(t provider.T) {
|
||||
productURL = endpoint
|
||||
})
|
||||
|
||||
t.WithNewStep("start loms grpc server", func(st provider.StepCtx) {
|
||||
lis, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
st.Require().NoError(err)
|
||||
|
||||
s.lomsSrv = grpc.NewServer()
|
||||
pbLoms.RegisterLOMSServer(s.lomsSrv, &lomsServerMock{})
|
||||
|
||||
go s.lomsSrv.Serve(lis)
|
||||
|
||||
conn, err := grpc.Dial(lis.Addr().String(), grpc.WithInsecure(), grpc.WithBlock())
|
||||
st.Require().NoError(err)
|
||||
s.lomsConn = conn
|
||||
})
|
||||
|
||||
t.WithNewStep("init cart-service", func(sCtx provider.StepCtx) {
|
||||
prodClient := productsService.NewProductService(
|
||||
*http.DefaultClient,
|
||||
@@ -79,8 +113,10 @@ func (s *CartHandlerSuite) BeforeAll(t provider.T) {
|
||||
productURL,
|
||||
)
|
||||
|
||||
lomsClient := pbLoms.NewLOMSClient(s.lomsConn)
|
||||
|
||||
repo := repository.NewInMemoryRepository(10)
|
||||
s.cartSvc = cartService.NewCartService(repo, prodClient)
|
||||
s.cartSvc = cartService.NewCartService(repo, prodClient, lomsService.NewLomsService(lomsClient))
|
||||
|
||||
appSrv := &app.App{}
|
||||
|
||||
@@ -89,8 +125,11 @@ func (s *CartHandlerSuite) BeforeAll(t provider.T) {
|
||||
}
|
||||
|
||||
func (s *CartHandlerSuite) AfterAll(t provider.T) {
|
||||
_ = s.psContainer.Terminate(context.Background())
|
||||
s.server.Close()
|
||||
s.lomsSrv.Stop()
|
||||
s.lomsConn.Close()
|
||||
|
||||
_ = s.psContainer.Terminate(context.Background())
|
||||
}
|
||||
|
||||
// DELETE /user/<user_id>/cart/<sku_id>
|
||||
|
||||
Reference in New Issue
Block a user