package service import ( "context" "errors" "testing" "github.com/gojuno/minimock/v3" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "route256/loms/internal/domain/entity" "route256/loms/internal/domain/model" mock "route256/loms/internal/domain/service/mock" pb "route256/pkg/api/loms/v1" ) const ( testUser = entity.ID(1337) testSku = entity.Sku(199) ) type mockTxManager struct{} func (t *mockTxManager) WriteWithTransaction(ctx context.Context, fn func(ctx context.Context) error) (err error) { return fn(ctx) } func (t *mockTxManager) ReadWithTransaction(ctx context.Context, fn func(ctx context.Context) error) (err error) { return fn(ctx) } func (t *mockTxManager) WriteWithRepeatableRead(ctx context.Context, fn func(ctx context.Context) error) (err error) { return fn(ctx) } func (t *mockTxManager) ReadWithRepeatableRead(ctx context.Context, fn func(ctx context.Context) error) (err error) { return fn(ctx) } func TestLomsService_OrderCreate(t *testing.T) { t.Parallel() ctx := context.Background() mc := minimock.NewController(t) goodReq := &pb.OrderCreateRequest{ UserId: int64(testUser), Items: []*pb.OrderItem{ { Sku: int64(testSku), Count: 2, }, }, } badItemReq := &pb.OrderCreateRequest{ UserId: int64(testUser), Items: []*pb.OrderItem{ { Sku: 0, Count: 0, }, }, } type fields struct { orders OrderRepository stocks StockRepository } type args struct { req *pb.OrderCreateRequest } tests := []struct { name string fields fields args args wantErr require.ErrorAssertionFunc }{ { name: "success", fields: fields{ orders: mock.NewOrderRepositoryMock(mc). OrderCreateMock.Return(1, nil). OrderSetStatusMock.Return(nil), stocks: mock.NewStockRepositoryMock(mc). StockReserveMock.Return(nil), }, args: args{ req: goodReq, }, wantErr: require.NoError, }, { name: "invalid input", fields: fields{}, args: args{ req: &pb.OrderCreateRequest{ UserId: 0, }, }, wantErr: func(t require.TestingT, err error, _ ...interface{}) { require.ErrorIs(t, err, model.ErrInvalidInput) }, }, { name: "invalid input with bad items", fields: fields{}, args: args{ req: badItemReq, }, wantErr: func(t require.TestingT, err error, _ ...interface{}) { require.ErrorIs(t, err, model.ErrInvalidInput) }, }, { name: "order create error", fields: fields{ orders: mock.NewOrderRepositoryMock(mc). OrderCreateMock.Return(0, errors.New("order create error")), stocks: nil, }, args: args{ req: goodReq, }, wantErr: require.Error, }, { name: "stock reserve error", fields: fields{ orders: mock.NewOrderRepositoryMock(mc). OrderCreateMock.Return(1, nil). OrderSetStatusMock.Return(errors.New("status update error")), stocks: mock.NewStockRepositoryMock(mc). StockReserveMock.Return(errors.New("reservation error")), }, args: args{ req: goodReq, }, wantErr: require.Error, }, { name: "final status update error", fields: fields{ orders: mock.NewOrderRepositoryMock(mc). OrderCreateMock.Return(1, nil). OrderSetStatusMock.Return(errors.New("unexpected error")), stocks: mock.NewStockRepositoryMock(mc). StockReserveMock.Return(nil), }, args: args{ req: goodReq, }, wantErr: require.Error, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() svc := NewLomsService(tt.fields.orders, tt.fields.stocks, &mockTxManager{}) _, err := svc.OrderCreate(ctx, tt.args.req) tt.wantErr(t, err) }) } } func TestLomsService_OrderPay(t *testing.T) { t.Parallel() ctx := context.Background() mc := minimock.NewController(t) awaitingOrder := &entity.Order{ OrderID: 1, Status: pb.OrderStatus_ORDER_STATUS_AWAITING_PAYMENT.String(), Items: []entity.OrderItem{{ ID: testSku, Count: 2, }}, } payedOrder := &entity.Order{OrderID: 2, Status: pb.OrderStatus_ORDER_STATUS_PAYED.String()} badStatusOrder := &entity.Order{OrderID: 3, Status: pb.OrderStatus_ORDER_STATUS_FAILED.String()} type fields struct { orders OrderRepository stocks StockRepository } type args struct { id entity.ID } tests := []struct { name string fields fields args args wantErr require.ErrorAssertionFunc }{ { name: "success", fields: fields{ orders: mock.NewOrderRepositoryMock(mc). OrderGetByIDMock.Return(awaitingOrder, nil). OrderSetStatusMock.Return(nil), stocks: mock.NewStockRepositoryMock(mc). StockReserveRemoveMock.Return(nil), }, args: args{ id: awaitingOrder.OrderID, }, wantErr: require.NoError, }, { name: "invalid input", fields: fields{ orders: mock.NewOrderRepositoryMock(mc), stocks: mock.NewStockRepositoryMock(mc), }, args: args{ id: 0, }, wantErr: func(t require.TestingT, err error, _ ...interface{}) { require.ErrorIs(t, err, model.ErrInvalidInput) }, }, { name: "already payed", fields: fields{ orders: mock.NewOrderRepositoryMock(mc). OrderGetByIDMock.Return(payedOrder, nil), }, args: args{ id: payedOrder.OrderID, }, wantErr: require.NoError, }, { name: "invalid status", fields: fields{ orders: mock.NewOrderRepositoryMock(mc). OrderGetByIDMock.Return(badStatusOrder, nil), }, args: args{ id: badStatusOrder.OrderID, }, wantErr: func(t require.TestingT, err error, _ ...interface{}) { require.ErrorIs(t, err, model.ErrOrderInvalidStatus) }, }, { name: "unexpected logged error on updating stocks reserves", fields: fields{ orders: mock.NewOrderRepositoryMock(mc). OrderGetByIDMock.Return(awaitingOrder, nil). OrderSetStatusMock.Return(nil), stocks: mock.NewStockRepositoryMock(mc). StockReserveRemoveMock.Return(errors.New("unexpected error")), }, args: args{ id: badStatusOrder.OrderID, }, wantErr: require.NoError, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() svc := NewLomsService(tt.fields.orders, tt.fields.stocks, &mockTxManager{}) err := svc.OrderPay(ctx, tt.args.id) tt.wantErr(t, err) }) } } func TestLomsService_OrderInfoBadInput(t *testing.T) { t.Parallel() svc := NewLomsService( nil, nil, &mockTxManager{}, ) _, err := svc.OrderInfo(context.Background(), 0) require.ErrorIs(t, err, model.ErrInvalidInput) } func TestLomsService_OrderInfoSuccess(t *testing.T) { t.Parallel() order := &entity.Order{ OrderID: 123, Status: "payed", UserID: 1337, Items: []entity.OrderItem{}, } mc := minimock.NewController(t) svc := NewLomsService( mock.NewOrderRepositoryMock(mc).OrderGetByIDMock.Return(order, nil), nil, &mockTxManager{}, ) gotOrder, err := svc.OrderInfo(context.Background(), 123) require.NoError(t, err) require.Equal(t, order, gotOrder) } func TestLomsService_OrderCancel(t *testing.T) { t.Parallel() ctx := context.Background() mc := minimock.NewController(t) awaiting := &entity.Order{ OrderID: 1, Status: pb.OrderStatus_ORDER_STATUS_AWAITING_PAYMENT.String(), Items: []entity.OrderItem{{ ID: testSku, Count: 1, }}, } cancelled := &entity.Order{ OrderID: 2, Status: pb.OrderStatus_ORDER_STATUS_CANCELLED.String(), } payed := &entity.Order{ OrderID: 3, Status: pb.OrderStatus_ORDER_STATUS_PAYED.String(), } type fields struct { orders OrderRepository stocks StockRepository } type args struct { id entity.ID } tests := []struct { name string fields fields args args wantErr require.ErrorAssertionFunc }{ { name: "success", fields: fields{ orders: mock.NewOrderRepositoryMock(mc). OrderGetByIDMock.Return(awaiting, nil). OrderSetStatusMock.Return(nil), stocks: mock.NewStockRepositoryMock(mc). StockCancelMock.Return(nil), }, args: args{ id: awaiting.OrderID, }, wantErr: require.NoError, }, { name: "invalid input", fields: fields{ orders: mock.NewOrderRepositoryMock(mc), stocks: mock.NewStockRepositoryMock(mc), }, args: args{ id: 0, }, wantErr: func(t require.TestingT, err error, _ ...interface{}) { require.ErrorIs(t, err, model.ErrInvalidInput) }, }, { name: "already cancelled", fields: fields{ orders: mock.NewOrderRepositoryMock(mc). OrderGetByIDMock.Return(cancelled, nil), }, args: args{ id: cancelled.OrderID, }, wantErr: require.NoError, }, { name: "invalid status", fields: fields{ orders: mock.NewOrderRepositoryMock(mc). OrderGetByIDMock.Return(payed, nil), }, args: args{ id: payed.OrderID, }, wantErr: func(t require.TestingT, err error, _ ...interface{}) { require.ErrorIs(t, err, model.ErrOrderInvalidStatus) }, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() svc := NewLomsService(tt.fields.orders, tt.fields.stocks, &mockTxManager{}) err := svc.OrderCancel(ctx, tt.args.id) tt.wantErr(t, err) }) } } func TestLomsService_StocksInfo(t *testing.T) { t.Parallel() ctx := context.Background() mc := minimock.NewController(t) type fields struct{ stocks StockRepository } type args struct{ sku entity.Sku } tests := []struct { name string fields fields args args want uint32 wantErr require.ErrorAssertionFunc }{ { name: "success", fields: fields{ stocks: mock.NewStockRepositoryMock(mc). StockGetByIDMock.Return(&entity.Stock{ Item: entity.OrderItem{ Count: 7, }, }, nil), }, args: args{ sku: testSku, }, want: 7, wantErr: require.NoError, }, { name: "invalid sku", fields: fields{}, args: args{ sku: 0, }, wantErr: func(t require.TestingT, err error, _ ...interface{}) { require.ErrorIs(t, err, model.ErrInvalidInput) }, }, { name: "get by id error", fields: fields{ stocks: mock.NewStockRepositoryMock(mc). StockGetByIDMock.Return(nil, errors.New("unexpected error")), }, args: args{ sku: testSku, }, wantErr: require.Error, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() svc := NewLomsService(nil, tt.fields.stocks, &mockTxManager{}) got, err := svc.StocksInfo(ctx, tt.args.sku) tt.wantErr(t, err) if err == nil { assert.Equal(t, tt.want, got) } }) } }