package service import ( "context" "errors" "testing" "github.com/gojuno/minimock/v3" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/goleak" "route256/cart/internal/domain/entity" "route256/cart/internal/domain/model" "route256/cart/internal/domain/service/mock" ) const ( validPrice = 123 validName = "some product name" ) type productServiceFake struct{} func (f *productServiceFake) GetProductBySku(_ context.Context, sku entity.Sku) (*model.Product, error) { if sku%2 == 0 { return nil, errors.New("empty shelf") } return &model.Product{ Name: validName, Price: validPrice, Sku: sku, }, nil } func (f *productServiceFake) GetProducts(ctx context.Context, skus []entity.Sku) ([]*model.Product, error) { res := make([]*model.Product, len(skus)) for i, sku := range skus { prod, err := f.GetProductBySku(ctx, sku) if err != nil { return nil, err } res[i] = prod } return res, 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 TestMain(m *testing.M) { goleak.VerifyTestMain(m) } func TestCartService_AddItem(t *testing.T) { t.Parallel() ctx := context.Background() mc := minimock.NewController(t) testSKU := entity.Sku(199) testItem := model.Item{ Product: &model.Product{ Name: validName, Price: 123, Sku: testSKU, }, 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 item *model.Item userID entity.UID } tests := []struct { name string fields fields args args wantErr require.ErrorAssertionFunc }{ { name: "success", fields: fields{ repository: mock.NewRepositoryMock(mc). AddItemMock. Expect(ctx, 1337, &testItem). Return(nil), productService: &productServiceFake{}, lomsService: &lomsServiceFake{}, }, args: args{ ctx: ctx, item: &testItem, userID: 1337, }, wantErr: require.NoError, }, { name: "invalid item", fields: fields{ repository: nil, productService: nil, }, args: args{ ctx: ctx, item: &model.Item{ Product: &model.Product{ Name: "name", Price: 123, Sku: 0, }, Count: 0, }, userID: 0, }, wantErr: require.Error, }, { name: "invalid user id", fields: fields{ repository: nil, productService: nil, }, args: args{ ctx: ctx, item: &testItem, userID: 0, }, wantErr: require.Error, }, { name: "product service error", fields: fields{ repository: nil, productService: &productServiceFake{}, lomsService: &lomsServiceFake{}, }, args: args{ ctx: ctx, item: &model.Item{ Product: &model.Product{ Name: "", Price: 0, Sku: 4, }, Count: 1, }, userID: 1337, }, wantErr: require.Error, }, { name: "repository error", fields: fields{ repository: mock.NewRepositoryMock(mc). AddItemMock. Expect(ctx, 1337, &testItem). Return(assert.AnError), productService: &productServiceFake{}, lomsService: &lomsServiceFake{}, }, args: args{ ctx: ctx, item: &testItem, userID: 1337, }, 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) { t.Parallel() 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) tt.wantErr(t, err, "check add review error") }) } } func TestCartService_GetItemsByUserID(t *testing.T) { t.Parallel() ctx := context.Background() mc := minimock.NewController(t) testUID := entity.UID(1337) testSKU := entity.Sku(199) testModelCart := model.Cart{ UserID: testUID, Items: []*model.Item{ { Product: &model.Product{ Name: validName, Price: validPrice, Sku: testSKU, }, Count: 1, }, }, TotalPrice: validPrice, } testEntityCart := entity.Cart{ UserID: testUID, Items: []entity.Sku{testSKU}, ItemCount: map[entity.Sku]uint32{testSKU: 1}, } type fields struct { repository Repository productService ProductService } type args struct { ctx context.Context userID entity.UID } tests := []struct { name string fields fields args args want *model.Cart wantErr require.ErrorAssertionFunc }{ { name: "success 1 item", fields: fields{ repository: mock.NewRepositoryMock(mc). GetItemsByUserIDMock. Expect(ctx, testUID). Return(testEntityCart, nil), productService: &productServiceFake{}, }, args: args{ ctx: ctx, userID: testUID, }, want: &testModelCart, wantErr: require.NoError, }, { name: "success 2 items", fields: fields{ repository: mock.NewRepositoryMock(mc). GetItemsByUserIDMock. Expect(ctx, testUID). Return(entity.Cart{ UserID: testUID, Items: []entity.Sku{testSKU}, ItemCount: map[entity.Sku]uint32{testSKU: 2}, }, nil), productService: &productServiceFake{}, }, args: args{ ctx: ctx, userID: testUID, }, want: &model.Cart{ UserID: testUID, Items: []*model.Item{ { Product: &model.Product{ Name: validName, Price: validPrice, Sku: testSKU, }, Count: 2, }, }, TotalPrice: validPrice * 2, }, wantErr: require.NoError, }, { name: "success 2 different items", fields: fields{ repository: mock.NewRepositoryMock(mc). GetItemsByUserIDMock. Expect(ctx, testUID). Return(entity.Cart{ UserID: testUID, Items: []entity.Sku{testSKU, 1}, ItemCount: map[entity.Sku]uint32{testSKU: 1, 1: 1}, }, nil), productService: &productServiceFake{}, }, args: args{ ctx: ctx, userID: testUID, }, want: &model.Cart{ UserID: testUID, Items: []*model.Item{ { Product: &model.Product{ Name: validName, Price: validPrice, Sku: 1, }, Count: 1, }, { Product: &model.Product{ Name: validName, Price: validPrice, Sku: testSKU, }, Count: 1, }, }, TotalPrice: validPrice * 2, }, wantErr: require.NoError, }, { name: "invalid user id", fields: fields{ repository: nil, productService: nil, }, args: args{ ctx: ctx, userID: 0, }, want: nil, wantErr: require.Error, }, { name: "repository error", fields: fields{ repository: mock.NewRepositoryMock(mc). GetItemsByUserIDMock. Expect(ctx, testUID). Return(entity.Cart{}, assert.AnError), productService: nil, }, args: args{ ctx: ctx, userID: testUID, }, want: nil, wantErr: require.Error, }, { name: "empty cart", fields: fields{ repository: mock.NewRepositoryMock(mc). GetItemsByUserIDMock. Expect(ctx, testUID). Return(entity.Cart{}, nil), productService: nil, }, args: args{ ctx: ctx, userID: testUID, }, want: nil, wantErr: require.Error, }, { name: "product service error", fields: fields{ repository: mock.NewRepositoryMock(mc). GetItemsByUserIDMock. Expect(ctx, testUID). Return(entity.Cart{ UserID: testUID, Items: []entity.Sku{2}, ItemCount: map[entity.Sku]uint32{2: 1}, }, nil), productService: &productServiceFake{}, }, args: args{ ctx: ctx, userID: testUID, }, want: nil, wantErr: require.Error, }, { name: "product service error", fields: fields{ repository: mock.NewRepositoryMock(mc). GetItemsByUserIDMock. Expect(ctx, testUID). Return(entity.Cart{ UserID: testUID, Items: []entity.Sku{2}, ItemCount: map[entity.Sku]uint32{2: 1}, }, nil), productService: &productServiceFake{}, }, args: args{ ctx: ctx, userID: testUID, }, want: nil, wantErr: require.Error, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() s := &CartService{ repository: tt.fields.repository, productService: tt.fields.productService, } got, err := s.GetItemsByUserID(tt.args.ctx, tt.args.userID) tt.wantErr(t, err, "check add review error") assert.True(t, assert.EqualExportedValues(t, tt.want, got), "got unexpected cart") }) } } func TestCartService_DeleteItem(t *testing.T) { t.Parallel() ctx := context.Background() mc := minimock.NewController(t) testSKU := entity.Sku(199) testUID := entity.UID(1337) type fields struct { repository Repository } type args struct { ctx context.Context userID entity.UID sku entity.Sku } tests := []struct { name string fields fields args args wantErr require.ErrorAssertionFunc }{ { name: "success", fields: fields{ repository: mock.NewRepositoryMock(mc). DeleteItemMock. Expect(ctx, testUID, testSKU). Return(nil), }, args: args{ ctx: ctx, userID: testUID, sku: testSKU, }, wantErr: require.NoError, }, { name: "invalid user id", fields: fields{ repository: nil, }, args: args{ ctx: ctx, userID: 0, sku: testSKU, }, wantErr: require.Error, }, { name: "invalid sku", fields: fields{ repository: nil, }, args: args{ ctx: ctx, userID: testUID, sku: 0, }, wantErr: require.Error, }, { name: "repository error", fields: fields{ repository: mock.NewRepositoryMock(mc). DeleteItemMock. Expect(ctx, testUID, testSKU). Return(assert.AnError), }, args: args{ ctx: ctx, sku: testSKU, userID: testUID, }, wantErr: require.Error, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() s := &CartService{ repository: tt.fields.repository, } err := s.DeleteItem(tt.args.ctx, tt.args.userID, tt.args.sku) tt.wantErr(t, err, "check add review error") }) } } func TestCartService_DeleteItemsByUserID(t *testing.T) { t.Parallel() ctx := context.Background() mc := minimock.NewController(t) testUID := entity.UID(1337) type fields struct { repository Repository } type args struct { ctx context.Context userID entity.UID } tests := []struct { name string fields fields args args wantErr require.ErrorAssertionFunc }{ { name: "success", fields: fields{ repository: mock.NewRepositoryMock(mc). DeleteItemsByUserIDMock. Expect(ctx, testUID). Return(nil), }, args: args{ ctx: ctx, userID: testUID, }, wantErr: require.NoError, }, { name: "invalid user id", fields: fields{ repository: nil, }, args: args{ ctx: ctx, userID: 0, }, wantErr: require.Error, }, { name: "repository error", fields: fields{ repository: mock.NewRepositoryMock(mc). DeleteItemsByUserIDMock. Expect(ctx, testUID). Return(assert.AnError), }, args: args{ ctx: ctx, userID: testUID, }, wantErr: require.Error, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() s := &CartService{ repository: tt.fields.repository, } err := s.DeleteItemsByUserID(tt.args.ctx, tt.args.userID) tt.wantErr(t, err, "check add review error") }) } } 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) } }) } }