diff --git a/go.mod b/go.mod index 7a826f4..be41a2e 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/go-playground/validator/v10 v10.19.0 github.com/golang-jwt/jwt/v5 v5.2.0 github.com/jackc/pgx/v5 v5.5.3 + github.com/stretchr/testify v1.9.0 go.uber.org/zap v1.27.0 golang.org/x/crypto v0.19.0 ) @@ -23,9 +24,12 @@ require ( github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/rogpeppe/go-internal v1.12.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect go.uber.org/multierr v1.10.0 // indirect golang.org/x/net v0.21.0 // indirect golang.org/x/sync v0.5.0 // indirect golang.org/x/sys v0.17.0 // indirect golang.org/x/text v0.14.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 1049a1b..a604cf9 100644 --- a/go.sum +++ b/go.sum @@ -27,16 +27,24 @@ github.com/jackc/pgx/v5 v5.5.3 h1:Ces6/M3wbDXYpM8JyyPD57ivTtJACFZJd885pdIaV2s= github.com/jackc/pgx/v5 v5.5.3/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= @@ -54,6 +62,8 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/handlers/balance.go b/internal/handlers/balance.go index e802959..78904b0 100644 --- a/internal/handlers/balance.go +++ b/internal/handlers/balance.go @@ -113,6 +113,11 @@ func WithdrawOrder(s Storage) http.HandlerFunc { return } + if req.Sum <= 0 { + http.Error(w, "Invalid sum", http.StatusBadRequest) + return + } + currentBalance, err := s.GetUsersCurrentBalance(r.Context(), userID) if err != nil { logger.Log.Error("Error getting users current balance", zap.Error(err)) @@ -135,7 +140,7 @@ func WithdrawOrder(s Storage) http.HandlerFunc { order := &models.Order{ ID: orderID, UserID: userID, - Status: models.OrderStatusNew, + Status: models.OrderStatusProcessed, Sum: -req.Sum, UploadedAt: time.Now(), } diff --git a/internal/handlers/balance_test.go b/internal/handlers/balance_test.go new file mode 100644 index 0000000..a3e571b --- /dev/null +++ b/internal/handlers/balance_test.go @@ -0,0 +1,496 @@ +package handlers + +import ( + "encoding/json" + "github.com/go-chi/chi/v5" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/vindosVP/loyalty-system/internal/handlers/mocks" + "github.com/vindosVP/loyalty-system/internal/models" + "github.com/vindosVP/loyalty-system/internal/storage" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" +) + +func TestGetUsersBalance(t *testing.T) { + uri := "/api/user/balance" + + type request struct { + method string + userID string + } + type getUsersCurrentBalanceMock struct { + needed bool + result float64 + err error + } + type getUsersWithdrawnBalanceMock struct { + needed bool + result float64 + err error + } + type want struct { + statusCode int + result BalanceResponse + } + + tests := []struct { + name string + request request + getUsersCurrentBalanceMock getUsersCurrentBalanceMock + getUsersWithdrawnBalanceMock getUsersWithdrawnBalanceMock + want want + }{ + { + name: "ok", + request: request{ + method: http.MethodGet, + userID: "1", + }, + getUsersCurrentBalanceMock: getUsersCurrentBalanceMock{ + needed: true, + result: 100, + err: nil, + }, + getUsersWithdrawnBalanceMock: getUsersWithdrawnBalanceMock{ + needed: true, + result: 50, + err: nil, + }, + want: want{ + statusCode: http.StatusOK, + result: BalanceResponse{ + Current: 100, + Withdrawn: 50, + }, + }, + }, + { + name: "wrong method", + request: request{ + method: http.MethodPost, + userID: "1", + }, + getUsersCurrentBalanceMock: getUsersCurrentBalanceMock{ + needed: false, + result: 0, + err: nil, + }, + getUsersWithdrawnBalanceMock: getUsersWithdrawnBalanceMock{ + needed: false, + result: 0, + err: nil, + }, + want: want{ + statusCode: http.StatusMethodNotAllowed, + result: BalanceResponse{ + Current: 0, + Withdrawn: 0, + }, + }, + }, + { + name: "zero balance", + request: request{ + method: http.MethodGet, + userID: "1", + }, + getUsersCurrentBalanceMock: getUsersCurrentBalanceMock{ + needed: true, + result: 0, + err: nil, + }, + getUsersWithdrawnBalanceMock: getUsersWithdrawnBalanceMock{ + needed: true, + result: 0, + err: nil, + }, + want: want{ + statusCode: http.StatusOK, + result: BalanceResponse{ + Current: 0, + Withdrawn: 0, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := mocks.NewStorage(t) + if tt.getUsersCurrentBalanceMock.needed { + s.On("GetUsersCurrentBalance", mock.Anything, mock.Anything).Return(tt.getUsersCurrentBalanceMock.result, tt.getUsersCurrentBalanceMock.err) + } + if tt.getUsersWithdrawnBalanceMock.needed { + s.On("GetUsersWithdrawnBalance", mock.Anything, mock.Anything).Return(tt.getUsersWithdrawnBalanceMock.result, tt.getUsersWithdrawnBalanceMock.err) + } + + r := chi.NewRouter() + r.Get("/api/user/balance", GetUsersBalance(s)) + + req := httptest.NewRequest(tt.request.method, uri, nil) + req.Header.Set("x-user-id", tt.request.userID) + w := httptest.NewRecorder() + r.ServeHTTP(w, req) + res := w.Result() + defer res.Body.Close() + + assert.Equal(t, tt.want.statusCode, res.StatusCode) + if tt.want.statusCode == http.StatusOK { + var response BalanceResponse + err := json.NewDecoder(res.Body).Decode(&response) + assert.NoError(t, err) + assert.Equal(t, tt.want.result, response) + } + }) + } +} + +func TestWithdrawOrder(t *testing.T) { + uri := "/api/user/withdraw" + + type request struct { + method string + userID string + body string + } + type getUsersCurrentBalanceMock struct { + needed bool + result float64 + err error + } + type createOrderMock struct { + needed bool + result *models.Order + err error + } + type want struct { + statusCode int + } + + tests := []struct { + name string + request request + getUsersCurrentBalanceMock getUsersCurrentBalanceMock + createOrderMock createOrderMock + want want + }{ + { + name: "ok", + request: request{ + method: http.MethodPost, + userID: "1", + body: "{\"order\": \"8023459525\", \"sum\": 100}", + }, + getUsersCurrentBalanceMock: getUsersCurrentBalanceMock{ + needed: true, + result: 200, + err: nil, + }, + createOrderMock: createOrderMock{ + needed: true, + result: &models.Order{ + ID: 1, + UserID: 8023459525, + Status: models.OrderStatusProcessed, + Sum: -100, + UploadedAt: time.Now(), + }, + err: nil, + }, + want: want{ + statusCode: http.StatusOK, + }, + }, + { + name: "wrong method", + request: request{ + method: http.MethodGet, + userID: "1", + body: "{\"order\": \"8023459525\", \"sum\": 100}", + }, + getUsersCurrentBalanceMock: getUsersCurrentBalanceMock{ + needed: false, + result: 0, + err: nil, + }, + createOrderMock: createOrderMock{ + needed: false, + result: nil, + err: nil, + }, + want: want{ + statusCode: http.StatusMethodNotAllowed, + }, + }, + { + name: "wrong order number", + request: request{ + method: http.MethodPost, + userID: "1", + body: "{\"order\": \"1111\", \"sum\": 100}", + }, + getUsersCurrentBalanceMock: getUsersCurrentBalanceMock{ + needed: true, + result: 200, + err: nil, + }, + createOrderMock: createOrderMock{ + needed: false, + result: nil, + err: nil, + }, + want: want{ + statusCode: http.StatusUnprocessableEntity, + }, + }, + { + name: "wrong sum", + request: request{ + method: http.MethodPost, + userID: "1", + body: "{\"order\": \"8023459525\", \"sum\": -100}", + }, + getUsersCurrentBalanceMock: getUsersCurrentBalanceMock{ + needed: false, + result: 0, + err: nil, + }, + createOrderMock: createOrderMock{ + needed: false, + result: nil, + err: nil, + }, + want: want{ + statusCode: http.StatusBadRequest, + }, + }, + { + name: "not enough balance", + request: request{ + method: http.MethodPost, + userID: "1", + body: "{\"order\": \"8023459525\", \"sum\": 100}", + }, + getUsersCurrentBalanceMock: getUsersCurrentBalanceMock{ + needed: true, + result: 50, + err: nil, + }, + createOrderMock: createOrderMock{ + needed: false, + result: nil, + err: nil, + }, + want: want{ + statusCode: http.StatusPaymentRequired, + }, + }, + { + name: "order already exists", + request: request{ + method: http.MethodPost, + userID: "1", + body: "{\"order\": \"8023459525\", \"sum\": 100}", + }, + getUsersCurrentBalanceMock: getUsersCurrentBalanceMock{ + needed: true, + result: 200, + err: nil, + }, + createOrderMock: createOrderMock{ + needed: true, + result: nil, + err: storage.ErrOrderAlreadyExists, + }, + want: want{ + statusCode: http.StatusConflict, + }, + }, + { + name: "order already created by other user", + request: request{ + method: http.MethodPost, + userID: "1", + body: "{\"order\": \"8023459525\", \"sum\": 100}", + }, + getUsersCurrentBalanceMock: getUsersCurrentBalanceMock{ + needed: true, + result: 200, + err: nil, + }, + createOrderMock: createOrderMock{ + needed: true, + result: nil, + err: storage.ErrOrderCreatedByOtherUser, + }, + want: want{ + statusCode: http.StatusConflict, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := mocks.NewStorage(t) + if tt.getUsersCurrentBalanceMock.needed { + s.On("GetUsersCurrentBalance", mock.Anything, mock.Anything).Return(tt.getUsersCurrentBalanceMock.result, tt.getUsersCurrentBalanceMock.err) + } + if tt.createOrderMock.needed { + s.On("CreateOrder", mock.Anything, mock.Anything).Return(tt.createOrderMock.result, tt.createOrderMock.err) + } + + r := chi.NewRouter() + r.Post(uri, WithdrawOrder(s)) + req := httptest.NewRequest(tt.request.method, uri, strings.NewReader(tt.request.body)) + req.Header.Set("x-user-id", tt.request.userID) + w := httptest.NewRecorder() + r.ServeHTTP(w, req) + res := w.Result() + defer res.Body.Close() + + assert.Equal(t, tt.want.statusCode, res.StatusCode) + }) + } +} + +func TestGetUsersWithdrawals(t *testing.T) { + uri := "/api/user/withdrawals" + currentTime := time.Now() + + type request struct { + method string + userID string + } + type want struct { + statusCode int + result WithdrawalResponse + } + type getUsersWithdrawalsMock struct { + needed bool + result []*models.Order + err error + } + + tests := []struct { + name string + request request + getUsersWithdrawalsMock getUsersWithdrawalsMock + want want + }{ + { + name: "ok", + request: request{ + method: http.MethodGet, + userID: "1", + }, + getUsersWithdrawalsMock: getUsersWithdrawalsMock{ + needed: true, + result: []*models.Order{ + { + ID: 1, + UserID: 1, + Status: models.OrderStatusProcessed, + Sum: 200, + UploadedAt: currentTime, + }, + { + ID: 2, + UserID: 1, + Status: models.OrderStatusProcessed, + Sum: 1, + UploadedAt: currentTime, + }, { + ID: 3, + UserID: 1, + Status: models.OrderStatusProcessed, + Sum: 1000, + UploadedAt: currentTime, + }, + }, + err: nil, + }, + want: want{ + statusCode: http.StatusOK, + result: WithdrawalResponse{ + &WithdrawalOrder{ + OrderID: "1", + Sum: 200, + ProcessedAt: currentTime.Format(time.RFC3339), + }, + &WithdrawalOrder{ + OrderID: "2", + Sum: 1, + ProcessedAt: currentTime.Format(time.RFC3339), + }, + &WithdrawalOrder{ + OrderID: "3", + Sum: 1000, + ProcessedAt: currentTime.Format(time.RFC3339), + }, + }, + }, + }, + { + name: "no withdrawals", + request: request{ + method: http.MethodGet, + userID: "1", + }, + getUsersWithdrawalsMock: getUsersWithdrawalsMock{ + needed: true, + result: make([]*models.Order, 0), + err: nil, + }, + want: want{ + statusCode: http.StatusNoContent, + result: nil, + }, + }, + { + name: "wrong method", + request: request{ + method: http.MethodPost, + userID: "1", + }, + getUsersWithdrawalsMock: getUsersWithdrawalsMock{ + needed: false, + result: nil, + err: nil, + }, + want: want{ + statusCode: http.StatusMethodNotAllowed, + result: nil, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := mocks.NewStorage(t) + if tt.getUsersWithdrawalsMock.needed { + s.On("GetUsersWithdrawals", mock.Anything, mock.Anything).Return(tt.getUsersWithdrawalsMock.result, tt.getUsersWithdrawalsMock.err) + } + + r := chi.NewRouter() + r.Get(uri, GetUsersWithdrawals(s)) + req := httptest.NewRequest(tt.request.method, uri, nil) + req.Header.Set("x-user-id", tt.request.userID) + w := httptest.NewRecorder() + r.ServeHTTP(w, req) + res := w.Result() + defer res.Body.Close() + + assert.Equal(t, tt.want.statusCode, res.StatusCode) + if tt.want.statusCode == http.StatusOK { + var response WithdrawalResponse + err := json.NewDecoder(res.Body).Decode(&response) + assert.NoError(t, err) + assert.ElementsMatch(t, tt.want.result, response) + } + }) + } +} diff --git a/internal/handlers/interface.go b/internal/handlers/interface.go index 3ecf220..c8a1e9a 100644 --- a/internal/handlers/interface.go +++ b/internal/handlers/interface.go @@ -5,6 +5,7 @@ import ( "github.com/vindosVP/loyalty-system/internal/models" ) +//go:generate go run github.com/vektra/mockery/v2@v2.28.2 --name=Storage type Storage interface { CreateUser(ctx context.Context, user *models.User) (*models.User, error) GetUserByLogin(ctx context.Context, login string) (*models.User, error) diff --git a/internal/handlers/mocks/Storage.go b/internal/handlers/mocks/Storage.go new file mode 100644 index 0000000..3a4d19d --- /dev/null +++ b/internal/handlers/mocks/Storage.go @@ -0,0 +1,209 @@ +// Code generated by mockery v2.28.2. DO NOT EDIT. + +package mocks + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" + + models "github.com/vindosVP/loyalty-system/internal/models" +) + +// Storage is an autogenerated mock type for the Storage type +type Storage struct { + mock.Mock +} + +// CreateOrder provides a mock function with given fields: ctx, order +func (_m *Storage) CreateOrder(ctx context.Context, order *models.Order) (*models.Order, error) { + ret := _m.Called(ctx, order) + + var r0 *models.Order + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *models.Order) (*models.Order, error)); ok { + return rf(ctx, order) + } + if rf, ok := ret.Get(0).(func(context.Context, *models.Order) *models.Order); ok { + r0 = rf(ctx, order) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.Order) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *models.Order) error); ok { + r1 = rf(ctx, order) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CreateUser provides a mock function with given fields: ctx, user +func (_m *Storage) CreateUser(ctx context.Context, user *models.User) (*models.User, error) { + ret := _m.Called(ctx, user) + + var r0 *models.User + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *models.User) (*models.User, error)); ok { + return rf(ctx, user) + } + if rf, ok := ret.Get(0).(func(context.Context, *models.User) *models.User); ok { + r0 = rf(ctx, user) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.User) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *models.User) error); ok { + r1 = rf(ctx, user) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetUserByLogin provides a mock function with given fields: ctx, login +func (_m *Storage) GetUserByLogin(ctx context.Context, login string) (*models.User, error) { + ret := _m.Called(ctx, login) + + var r0 *models.User + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (*models.User, error)); ok { + return rf(ctx, login) + } + if rf, ok := ret.Get(0).(func(context.Context, string) *models.User); ok { + r0 = rf(ctx, login) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.User) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, login) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetUsersCurrentBalance provides a mock function with given fields: ctx, userID +func (_m *Storage) GetUsersCurrentBalance(ctx context.Context, userID int) (float64, error) { + ret := _m.Called(ctx, userID) + + var r0 float64 + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int) (float64, error)); ok { + return rf(ctx, userID) + } + if rf, ok := ret.Get(0).(func(context.Context, int) float64); ok { + r0 = rf(ctx, userID) + } else { + r0 = ret.Get(0).(float64) + } + + if rf, ok := ret.Get(1).(func(context.Context, int) error); ok { + r1 = rf(ctx, userID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetUsersOrders provides a mock function with given fields: ctx, userID +func (_m *Storage) GetUsersOrders(ctx context.Context, userID int) ([]*models.Order, error) { + ret := _m.Called(ctx, userID) + + var r0 []*models.Order + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int) ([]*models.Order, error)); ok { + return rf(ctx, userID) + } + if rf, ok := ret.Get(0).(func(context.Context, int) []*models.Order); ok { + r0 = rf(ctx, userID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*models.Order) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, int) error); ok { + r1 = rf(ctx, userID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetUsersWithdrawals provides a mock function with given fields: ctx, userID +func (_m *Storage) GetUsersWithdrawals(ctx context.Context, userID int) ([]*models.Order, error) { + ret := _m.Called(ctx, userID) + + var r0 []*models.Order + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int) ([]*models.Order, error)); ok { + return rf(ctx, userID) + } + if rf, ok := ret.Get(0).(func(context.Context, int) []*models.Order); ok { + r0 = rf(ctx, userID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*models.Order) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, int) error); ok { + r1 = rf(ctx, userID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetUsersWithdrawnBalance provides a mock function with given fields: ctx, userID +func (_m *Storage) GetUsersWithdrawnBalance(ctx context.Context, userID int) (float64, error) { + ret := _m.Called(ctx, userID) + + var r0 float64 + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int) (float64, error)); ok { + return rf(ctx, userID) + } + if rf, ok := ret.Get(0).(func(context.Context, int) float64); ok { + r0 = rf(ctx, userID) + } else { + r0 = ret.Get(0).(float64) + } + + if rf, ok := ret.Get(1).(func(context.Context, int) error); ok { + r1 = rf(ctx, userID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +type mockConstructorTestingTNewStorage interface { + mock.TestingT + Cleanup(func()) +} + +// NewStorage creates a new instance of Storage. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewStorage(t mockConstructorTestingTNewStorage) *Storage { + mock := &Storage{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/handlers/orders_test.go b/internal/handlers/orders_test.go new file mode 100644 index 0000000..4108a70 --- /dev/null +++ b/internal/handlers/orders_test.go @@ -0,0 +1,307 @@ +package handlers + +import ( + "encoding/json" + "github.com/go-chi/chi/v5" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/vindosVP/loyalty-system/internal/handlers/mocks" + "github.com/vindosVP/loyalty-system/internal/models" + "github.com/vindosVP/loyalty-system/internal/storage" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" +) + +func TestCreateOrder(t *testing.T) { + uri := "/api/user/orders" + + type request struct { + method string + body string + userID string + } + type want struct { + statusCode int + } + type createOrderMock struct { + needed bool + result *models.Order + err error + } + + tests := []struct { + name string + createOrderMock createOrderMock + request request + want want + }{ + { + name: "ok", + createOrderMock: createOrderMock{ + needed: true, + result: &models.Order{ + ID: 7703824164, + UserID: 1, + Status: models.OrderStatusNew, + Sum: 0, + UploadedAt: time.Now(), + }, + err: nil, + }, + request: request{ + method: http.MethodPost, + body: "7703824164", + userID: "1", + }, + want: want{ + statusCode: http.StatusAccepted, + }, + }, + { + name: "wrong method", + createOrderMock: createOrderMock{ + needed: false, + result: nil, + err: nil, + }, + request: request{ + method: http.MethodGet, + body: "7703824164", + userID: "1", + }, + want: want{ + statusCode: http.StatusMethodNotAllowed, + }, + }, + { + name: "invalid order number", + createOrderMock: createOrderMock{ + needed: false, + result: nil, + err: nil, + }, + request: request{ + method: http.MethodPost, + body: "1111", + userID: "1", + }, + want: want{ + statusCode: http.StatusUnprocessableEntity, + }, + }, + { + name: "empty order number", + createOrderMock: createOrderMock{ + needed: false, + result: nil, + err: nil, + }, + request: request{ + method: http.MethodPost, + body: "", + userID: "1", + }, + want: want{ + statusCode: http.StatusBadRequest, + }, + }, + { + name: "order already exists", + createOrderMock: createOrderMock{ + needed: true, + result: nil, + err: storage.ErrOrderAlreadyExists, + }, + request: request{ + method: http.MethodPost, + body: "7703824164", + userID: "1", + }, + want: want{ + statusCode: http.StatusOK, + }, + }, + { + name: "order already created by another user", + createOrderMock: createOrderMock{ + needed: true, + result: nil, + err: storage.ErrOrderCreatedByOtherUser, + }, + request: request{ + method: http.MethodPost, + body: "7703824164", + userID: "1", + }, + want: want{ + statusCode: http.StatusConflict, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := mocks.NewStorage(t) + if tt.createOrderMock.needed { + s.On("CreateOrder", mock.Anything, mock.Anything).Return(tt.createOrderMock.result, tt.createOrderMock.err) + } + + r := chi.NewRouter() + r.Post(uri, CreateOrder(s)) + + req := httptest.NewRequest(tt.request.method, uri, strings.NewReader(tt.request.body)) + req.Header.Set("x-user-id", tt.request.userID) + w := httptest.NewRecorder() + r.ServeHTTP(w, req) + res := w.Result() + defer res.Body.Close() + + assert.Equal(t, tt.want.statusCode, res.StatusCode) + }) + } +} + +func TestGetOrderList(t *testing.T) { + uri := "/api/users/orders" + currentTime := time.Now() + currentTimeStr := currentTime.Format(time.RFC3339) + + type request struct { + method string + userID string + } + type getUserOrdersMock struct { + needed bool + result []*models.Order + err error + } + type want struct { + statusCode int + result OrdersListResponse + } + + tests := []struct { + name string + getUserOrdersMock getUserOrdersMock + request request + want want + }{ + { + name: "ok", + getUserOrdersMock: getUserOrdersMock{ + needed: true, + result: []*models.Order{ + { + ID: 9278923470, + UserID: 1, + Status: models.OrderStatusProcessed, + Sum: 500, + UploadedAt: currentTime, + }, + { + ID: 12345678903, + UserID: 1, + Status: models.OrderStatusProcessing, + Sum: 0, + UploadedAt: currentTime, + }, + { + ID: 346436439, + UserID: 1, + Status: models.OrderStatusInvalid, + Sum: 0, + UploadedAt: currentTime, + }, + }, + err: nil, + }, + request: request{ + method: http.MethodGet, + userID: "1", + }, + want: want{ + statusCode: http.StatusOK, + result: OrdersListResponse{ + &OrderResponse{ + Number: "9278923470", + Status: "PROCESSED", + Accrual: 500, + UploadedAt: currentTimeStr, + }, + &OrderResponse{ + Number: "12345678903", + Status: "PROCESSING", + UploadedAt: currentTimeStr, + }, + &OrderResponse{ + Number: "346436439", + Status: "INVALID", + UploadedAt: currentTimeStr, + }, + }, + }, + }, + { + name: "wrong method", + getUserOrdersMock: getUserOrdersMock{ + needed: false, + result: nil, + err: nil, + }, + request: request{ + method: http.MethodPost, + userID: "1", + }, + want: want{ + statusCode: http.StatusMethodNotAllowed, + result: nil, + }, + }, + { + name: "no orders", + getUserOrdersMock: getUserOrdersMock{ + needed: true, + result: make([]*models.Order, 0), + err: nil, + }, + request: request{ + method: http.MethodGet, + userID: "1", + }, + want: want{ + statusCode: http.StatusNoContent, + result: nil, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := mocks.NewStorage(t) + if tt.getUserOrdersMock.needed { + s.On("GetUsersOrders", mock.Anything, mock.Anything).Return(tt.getUserOrdersMock.result, tt.getUserOrdersMock.err) + } + + r := chi.NewRouter() + r.Get(uri, GetOrderList(s)) + + req := httptest.NewRequest(tt.request.method, uri, nil) + req.Header.Set("x-user-id", tt.request.userID) + w := httptest.NewRecorder() + r.ServeHTTP(w, req) + res := w.Result() + defer res.Body.Close() + + assert.Equal(t, tt.want.statusCode, res.StatusCode) + if tt.want.statusCode == http.StatusOK { + var ordersListResponse OrdersListResponse + err := json.NewDecoder(res.Body).Decode(&ordersListResponse) + assert.NoError(t, err) + assert.ElementsMatch(t, tt.want.result, ordersListResponse) + } + }) + } +} diff --git a/internal/handlers/users.go b/internal/handlers/users.go index c4e1e46..500a714 100644 --- a/internal/handlers/users.go +++ b/internal/handlers/users.go @@ -64,7 +64,6 @@ func Login(s Storage, jwtSecret string) http.HandlerFunc { return } - w.Header().Set("Content-Type", "application/json") w.Header().Set("Authorization", fmt.Sprintf("Bearer %s", token)) w.WriteHeader(http.StatusOK) } @@ -94,6 +93,17 @@ func Register(s Storage, jwtSecret string) http.HandlerFunc { return } + foundUser, err := s.GetUserByLogin(r.Context(), user.Login) + if err != nil && !errors.Is(err, storage.ErrUserNotFound) { + logger.Log.Error("Error getting user", zap.Error(err)) + http.Error(w, "Error getting user", http.StatusInternalServerError) + return + } + if foundUser != nil { + http.Error(w, "User already exists", http.StatusConflict) + return + } + encPwd, err := passwords.Encrypt(user.Pwd) if err != nil { logger.Log.Error("Error encrypting password", zap.Error(err)) diff --git a/internal/handlers/users_test.go b/internal/handlers/users_test.go new file mode 100644 index 0000000..91d0b8a --- /dev/null +++ b/internal/handlers/users_test.go @@ -0,0 +1,384 @@ +package handlers + +import ( + "github.com/go-chi/chi/v5" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "github.com/vindosVP/loyalty-system/internal/handlers/mocks" + "github.com/vindosVP/loyalty-system/internal/models" + "github.com/vindosVP/loyalty-system/internal/storage" + "github.com/vindosVP/loyalty-system/pkg/passwords" + "github.com/vindosVP/loyalty-system/pkg/tokens" + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +func TestRegister(t *testing.T) { + jwtSecret := "superSecret" + uri := "/api/user/register" + + type request struct { + method string + body string + } + type want struct { + code int + checkJWT bool + userID string + } + type createUserMock struct { + needed bool + result *models.User + err error + } + type getUserByLoginMock struct { + needed bool + result *models.User + err error + } + + tests := []struct { + name string + createUserMock createUserMock + getUserByLoginMock getUserByLoginMock + request request + want want + }{ + { + name: "ok", + createUserMock: createUserMock{ + needed: true, + result: &models.User{ + ID: 1, + Login: "someLogin", + Pwd: "somePassword", + }, + err: nil, + }, + getUserByLoginMock: getUserByLoginMock{ + needed: true, + result: nil, + err: storage.ErrUserNotFound, + }, + request: request{ + method: http.MethodPost, + body: "{\"login\": \"someLogin\",\"password\": \"somePassword\"}", + }, + want: want{ + code: http.StatusOK, + checkJWT: true, + userID: "1", + }, + }, + { + name: "wrong method", + createUserMock: createUserMock{ + needed: false, + result: nil, + err: nil, + }, + getUserByLoginMock: getUserByLoginMock{ + needed: false, + result: nil, + err: nil, + }, + request: request{ + method: http.MethodGet, + body: "{\"login\": \"\",\"password\": \"somePassword\"}", + }, + want: want{ + code: http.StatusMethodNotAllowed, + checkJWT: false, + userID: "", + }, + }, + { + name: "invalid login", + createUserMock: createUserMock{ + needed: false, + result: nil, + err: nil, + }, + getUserByLoginMock: getUserByLoginMock{ + needed: false, + result: nil, + err: nil, + }, + request: request{ + method: http.MethodPost, + body: "{\"login\": \"\",\"password\": \"somePassword\"}", + }, + want: want{ + code: http.StatusBadRequest, + checkJWT: false, + userID: "", + }, + }, + { + name: "invalid password", + createUserMock: createUserMock{ + needed: false, + result: nil, + err: nil, + }, + getUserByLoginMock: getUserByLoginMock{ + needed: false, + result: nil, + err: nil, + }, + request: request{ + method: http.MethodPost, + body: "{\"login\": \"\",\"password\": \"somePassword\"}", + }, + want: want{ + code: http.StatusBadRequest, + checkJWT: false, + userID: "", + }, + }, + { + name: "user already exists", + createUserMock: createUserMock{ + needed: false, + result: nil, + err: nil, + }, + getUserByLoginMock: getUserByLoginMock{ + needed: true, + result: &models.User{ + ID: 1, + Login: "someLogin", + Pwd: "somePassword", + }, + err: nil, + }, + request: request{ + method: http.MethodPost, + body: "{\"login\": \"someLogin\",\"password\": \"somePassword\"}", + }, + want: want{ + code: http.StatusConflict, + checkJWT: false, + userID: "", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + s := mocks.NewStorage(t) + + if tt.getUserByLoginMock.needed { + s.On("GetUserByLogin", mock.Anything, mock.Anything).Return(tt.getUserByLoginMock.result, tt.getUserByLoginMock.err) + } + if tt.createUserMock.needed { + s.On("CreateUser", mock.Anything, mock.Anything).Return(tt.createUserMock.result, tt.createUserMock.err) + } + + r := chi.NewRouter() + r.Post(uri, Register(s, jwtSecret)) + + req := httptest.NewRequest(tt.request.method, uri, strings.NewReader(tt.request.body)) + w := httptest.NewRecorder() + r.ServeHTTP(w, req) + res := w.Result() + defer res.Body.Close() + + assert.Equal(t, tt.want.code, res.StatusCode) + if tt.want.checkJWT { + auth := res.Header.Get("Authorization") + require.NotEmpty(t, auth) + + splitToken := strings.Split(auth, "Bearer ") + require.Len(t, splitToken, 2) + token := splitToken[1] + + valid, err := tokens.IsAuthorized(token, jwtSecret) + require.NoError(t, err) + assert.True(t, valid) + + id, err := tokens.ExtractID(token, jwtSecret) + require.NoError(t, err) + assert.Equal(t, tt.want.userID, id) + } + }) + } +} + +func TestLogin(t *testing.T) { + jwtSecret := "superSecret" + uri := "/api/user/login" + encryptedSomePassword, _ := passwords.Encrypt("somePassword") + + type request struct { + method string + body string + } + type want struct { + code int + checkJWT bool + userID string + } + type getUserByLoginMock struct { + needed bool + result *models.User + err error + } + + tests := []struct { + name string + getUserByLoginMock getUserByLoginMock + request request + want want + }{ + { + name: "ok", + getUserByLoginMock: getUserByLoginMock{ + needed: true, + result: &models.User{ + ID: 1, + Login: "someLogin", + EncryptedPwd: encryptedSomePassword, + }, + err: nil, + }, + request: request{ + method: http.MethodPost, + body: "{\"login\": \"someLogin\",\"password\": \"somePassword\"}", + }, + want: want{ + code: http.StatusOK, + checkJWT: true, + userID: "1", + }, + }, + { + name: "wrong method", + getUserByLoginMock: getUserByLoginMock{ + needed: false, + result: nil, + err: nil, + }, + request: request{ + method: http.MethodGet, + body: "{\"login\": \"someLogin\",\"password\": \"somePassword\"}", + }, + want: want{ + code: http.StatusMethodNotAllowed, + checkJWT: false, + userID: "", + }, + }, + { + name: "invalid login", + getUserByLoginMock: getUserByLoginMock{ + needed: false, + result: nil, + err: nil, + }, + request: request{ + method: http.MethodPost, + body: "{\"login\": \"\",\"password\": \"somePassword\"}", + }, + want: want{ + code: http.StatusBadRequest, + checkJWT: false, + userID: "", + }, + }, + { + name: "invalid password", + getUserByLoginMock: getUserByLoginMock{ + needed: false, + result: nil, + err: nil, + }, + request: request{ + method: http.MethodPost, + body: "{\"login\": \"someLogin\",\"password\": \"\"}", + }, + want: want{ + code: http.StatusBadRequest, + checkJWT: false, + userID: "", + }, + }, + { + name: "user not found", + getUserByLoginMock: getUserByLoginMock{ + needed: true, + result: nil, + err: storage.ErrUserNotFound, + }, + request: request{ + method: http.MethodPost, + body: "{\"login\": \"someLogin\",\"password\": \"password\"}", + }, + want: want{ + code: http.StatusUnauthorized, + checkJWT: false, + userID: "", + }, + }, + { + name: "wrong password", + getUserByLoginMock: getUserByLoginMock{ + needed: true, + result: &models.User{ + ID: 1, + Login: "someLogin", + EncryptedPwd: "someWrongPassword", + }, + err: nil, + }, + request: request{ + method: http.MethodPost, + body: "{\"login\": \"someLogin\",\"password\": \"somePassword\"}", + }, + want: want{ + code: http.StatusUnauthorized, + checkJWT: false, + userID: "", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := mocks.NewStorage(t) + if tt.getUserByLoginMock.needed { + s.On("GetUserByLogin", mock.Anything, mock.Anything).Return(tt.getUserByLoginMock.result, tt.getUserByLoginMock.err) + } + + r := chi.NewRouter() + r.Post(uri, Login(s, jwtSecret)) + req := httptest.NewRequest(tt.request.method, uri, strings.NewReader(tt.request.body)) + w := httptest.NewRecorder() + r.ServeHTTP(w, req) + res := w.Result() + defer res.Body.Close() + + assert.Equal(t, tt.want.code, res.StatusCode) + if tt.want.checkJWT { + auth := res.Header.Get("Authorization") + require.NotEmpty(t, auth) + + splitToken := strings.Split(auth, "Bearer ") + require.Len(t, splitToken, 2) + token := splitToken[1] + + valid, err := tokens.IsAuthorized(token, jwtSecret) + require.NoError(t, err) + assert.True(t, valid) + + id, err := tokens.ExtractID(token, jwtSecret) + require.NoError(t, err) + assert.Equal(t, tt.want.userID, id) + } + }) + } +} diff --git a/internal/middleware/auth_test.go b/internal/middleware/auth_test.go new file mode 100644 index 0000000..37d7f3e --- /dev/null +++ b/internal/middleware/auth_test.go @@ -0,0 +1,135 @@ +package middleware + +import ( + "encoding/json" + "fmt" + "github.com/go-chi/chi/v5" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/vindosVP/loyalty-system/pkg/tokens" + "net/http" + "net/http/httptest" + "testing" + "time" +) + +func TestAuthenticator_WithAuth(t *testing.T) { + type AuthTestResponse struct { + UserID string + } + JWTSecret := "superSecret" + uri := "/testAuth" + + type user struct { + ID int + Login string + } + type auth struct { + addHeader bool + schema string + } + type want struct { + code int + result AuthTestResponse + } + + tests := []struct { + name string + auth auth + user user + want want + }{ + { + name: "ok", + auth: auth{ + addHeader: true, + schema: "Bearer", + }, + user: user{ + ID: 1, + Login: "someLogin", + }, + want: want{ + code: http.StatusOK, + result: AuthTestResponse{ + UserID: "1", + }, + }, + }, + { + name: "no auth header", + auth: auth{ + addHeader: false, + schema: "Bearer", + }, + user: user{ + ID: 1, + Login: "someLogin", + }, + want: want{ + code: http.StatusUnauthorized, + result: AuthTestResponse{ + UserID: "1", + }, + }, + }, + { + name: "wrong schema", + auth: auth{ + addHeader: false, + schema: "Basic", + }, + user: user{ + ID: 1, + Login: "someLogin", + }, + want: want{ + code: http.StatusUnauthorized, + result: AuthTestResponse{ + UserID: "1", + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + handler := func() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + userID := r.Header.Get("x-user-id") + resp := AuthTestResponse{UserID: userID} + data, _ := json.Marshal(&resp) + _, _ = w.Write(data) + w.WriteHeader(http.StatusOK) + } + } + + a := NewAuthenticator(JWTSecret) + + r := chi.NewRouter() + r.Use(a.WithAuth) + r.Get(uri, handler()) + + req := httptest.NewRequest("GET", uri, nil) + if tt.auth.addHeader { + token, err := tokens.CreateJWT( + tokens.JWTClaims(tt.user.ID, tt.user.Login, time.Now().Add(time.Hour*72).Unix()), JWTSecret) + require.NoError(t, err) + req.Header.Set("Authorization", fmt.Sprintf("%s %s", tt.auth.schema, token)) + } + w := httptest.NewRecorder() + r.ServeHTTP(w, req) + res := w.Result() + defer res.Body.Close() + + assert.Equal(t, tt.want.code, res.StatusCode) + if tt.want.code == http.StatusOK { + var resp AuthTestResponse + err := json.NewDecoder(res.Body).Decode(&resp) + require.NoError(t, err) + assert.Equal(t, tt.want.result, resp) + } + }) + } +} diff --git a/internal/models/order_test.go b/internal/models/order_test.go new file mode 100644 index 0000000..f49ac0f --- /dev/null +++ b/internal/models/order_test.go @@ -0,0 +1,64 @@ +package models + +import ( + "github.com/stretchr/testify/assert" + "testing" + "time" +) + +func TestOrder_Validate(t *testing.T) { + type args struct { + order *Order + } + type want struct { + valid bool + } + + tests := []struct { + name string + args args + want want + }{ + { + name: "valid", + args: args{ + order: &Order{ + ID: 7324401889, + UserID: 1, + Status: OrderStatusNew, + Sum: 0, + UploadedAt: time.Now(), + }, + }, + want: want{ + valid: true, + }, + }, + { + name: "invalid", + args: args{ + order: &Order{ + ID: 1111111111, + UserID: 1, + Status: OrderStatusNew, + Sum: 0, + UploadedAt: time.Now(), + }, + }, + want: want{ + valid: false, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.args.order.Validate() + if tt.want.valid { + assert.NoError(t, err) + } else { + assert.Error(t, err) + } + }) + } +} diff --git a/internal/models/user_test.go b/internal/models/user_test.go new file mode 100644 index 0000000..e9de77c --- /dev/null +++ b/internal/models/user_test.go @@ -0,0 +1,85 @@ +package models + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestUser_Validate(t *testing.T) { + type args struct { + user *User + } + type want struct { + valid bool + } + + tests := []struct { + name string + args args + want want + }{ + { + name: "valid", + args: args{ + user: &User{ + ID: 1, + Login: "someLogin", + Pwd: "somePassword", + }, + }, + want: want{ + valid: true, + }, + }, + { + name: "invalid login", + args: args{ + user: &User{ + ID: 1, + Login: "", + Pwd: "somePassword", + }, + }, + want: want{ + valid: false, + }, + }, + { + name: "invalid password", + args: args{ + user: &User{ + ID: 1, + Login: "someLogin", + Pwd: "", + }, + }, + want: want{ + valid: false, + }, + }, + { + name: "invalid login and password", + args: args{ + user: &User{ + ID: 1, + Login: "", + Pwd: "", + }, + }, + want: want{ + valid: false, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.args.user.Validate() + if tt.want.valid { + assert.NoError(t, err) + } else { + assert.Error(t, err) + } + }) + } +} diff --git a/internal/storage/mocks/OrderRepo.go b/internal/storage/mocks/OrderRepo.go new file mode 100644 index 0000000..c652eef --- /dev/null +++ b/internal/storage/mocks/OrderRepo.go @@ -0,0 +1,206 @@ +// Code generated by mockery v2.28.2. DO NOT EDIT. + +package mocks + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" + models "github.com/vindosVP/loyalty-system/internal/models" +) + +// OrderRepo is an autogenerated mock type for the OrderRepo type +type OrderRepo struct { + mock.Mock +} + +// Create provides a mock function with given fields: ctx, order +func (_m *OrderRepo) Create(ctx context.Context, order *models.Order) (*models.Order, error) { + ret := _m.Called(ctx, order) + + var r0 *models.Order + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *models.Order) (*models.Order, error)); ok { + return rf(ctx, order) + } + if rf, ok := ret.Get(0).(func(context.Context, *models.Order) *models.Order); ok { + r0 = rf(ctx, order) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.Order) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *models.Order) error); ok { + r1 = rf(ctx, order) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Exists provides a mock function with given fields: ctx, id +func (_m *OrderRepo) Exists(ctx context.Context, id int) (bool, error) { + ret := _m.Called(ctx, id) + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int) (bool, error)); ok { + return rf(ctx, id) + } + if rf, ok := ret.Get(0).(func(context.Context, int) bool); ok { + r0 = rf(ctx, id) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(context.Context, int) error); ok { + r1 = rf(ctx, id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetByID provides a mock function with given fields: ctx, id +func (_m *OrderRepo) GetByID(ctx context.Context, id int) (*models.Order, error) { + ret := _m.Called(ctx, id) + + var r0 *models.Order + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int) (*models.Order, error)); ok { + return rf(ctx, id) + } + if rf, ok := ret.Get(0).(func(context.Context, int) *models.Order); ok { + r0 = rf(ctx, id) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.Order) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, int) error); ok { + r1 = rf(ctx, id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetUsersCurrentBalance provides a mock function with given fields: ctx, userID +func (_m *OrderRepo) GetUsersCurrentBalance(ctx context.Context, userID int) (float64, error) { + ret := _m.Called(ctx, userID) + + var r0 float64 + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int) (float64, error)); ok { + return rf(ctx, userID) + } + if rf, ok := ret.Get(0).(func(context.Context, int) float64); ok { + r0 = rf(ctx, userID) + } else { + r0 = ret.Get(0).(float64) + } + + if rf, ok := ret.Get(1).(func(context.Context, int) error); ok { + r1 = rf(ctx, userID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetUsersOrders provides a mock function with given fields: ctx, userID +func (_m *OrderRepo) GetUsersOrders(ctx context.Context, userID int) ([]*models.Order, error) { + ret := _m.Called(ctx, userID) + + var r0 []*models.Order + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int) ([]*models.Order, error)); ok { + return rf(ctx, userID) + } + if rf, ok := ret.Get(0).(func(context.Context, int) []*models.Order); ok { + r0 = rf(ctx, userID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*models.Order) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, int) error); ok { + r1 = rf(ctx, userID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetUsersWithdrawals provides a mock function with given fields: ctx, userID +func (_m *OrderRepo) GetUsersWithdrawals(ctx context.Context, userID int) ([]*models.Order, error) { + ret := _m.Called(ctx, userID) + + var r0 []*models.Order + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int) ([]*models.Order, error)); ok { + return rf(ctx, userID) + } + if rf, ok := ret.Get(0).(func(context.Context, int) []*models.Order); ok { + r0 = rf(ctx, userID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*models.Order) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, int) error); ok { + r1 = rf(ctx, userID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetUsersWithdrawnBalance provides a mock function with given fields: ctx, userID +func (_m *OrderRepo) GetUsersWithdrawnBalance(ctx context.Context, userID int) (float64, error) { + ret := _m.Called(ctx, userID) + + var r0 float64 + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int) (float64, error)); ok { + return rf(ctx, userID) + } + if rf, ok := ret.Get(0).(func(context.Context, int) float64); ok { + r0 = rf(ctx, userID) + } else { + r0 = ret.Get(0).(float64) + } + + if rf, ok := ret.Get(1).(func(context.Context, int) error); ok { + r1 = rf(ctx, userID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +type mockConstructorTestingTNewOrderRepo interface { + mock.TestingT + Cleanup(func()) +} + +// NewOrderRepo creates a new instance of OrderRepo. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewOrderRepo(t mockConstructorTestingTNewOrderRepo) *OrderRepo { + mock := &OrderRepo{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/storage/mocks/UserRepo.go b/internal/storage/mocks/UserRepo.go new file mode 100644 index 0000000..e8e69fb --- /dev/null +++ b/internal/storage/mocks/UserRepo.go @@ -0,0 +1,132 @@ +// Code generated by mockery v2.28.2. DO NOT EDIT. + +package mocks + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" + models "github.com/vindosVP/loyalty-system/internal/models" +) + +// UserRepo is an autogenerated mock type for the UserRepo type +type UserRepo struct { + mock.Mock +} + +// Create provides a mock function with given fields: ctx, user +func (_m *UserRepo) Create(ctx context.Context, user *models.User) (*models.User, error) { + ret := _m.Called(ctx, user) + + var r0 *models.User + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *models.User) (*models.User, error)); ok { + return rf(ctx, user) + } + if rf, ok := ret.Get(0).(func(context.Context, *models.User) *models.User); ok { + r0 = rf(ctx, user) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.User) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *models.User) error); ok { + r1 = rf(ctx, user) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Exists provides a mock function with given fields: ctx, login +func (_m *UserRepo) Exists(ctx context.Context, login string) (bool, error) { + ret := _m.Called(ctx, login) + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (bool, error)); ok { + return rf(ctx, login) + } + if rf, ok := ret.Get(0).(func(context.Context, string) bool); ok { + r0 = rf(ctx, login) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, login) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetByID provides a mock function with given fields: ctx, id +func (_m *UserRepo) GetByID(ctx context.Context, id int) (*models.User, error) { + ret := _m.Called(ctx, id) + + var r0 *models.User + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int) (*models.User, error)); ok { + return rf(ctx, id) + } + if rf, ok := ret.Get(0).(func(context.Context, int) *models.User); ok { + r0 = rf(ctx, id) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.User) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, int) error); ok { + r1 = rf(ctx, id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetByLogin provides a mock function with given fields: ctx, login +func (_m *UserRepo) GetByLogin(ctx context.Context, login string) (*models.User, error) { + ret := _m.Called(ctx, login) + + var r0 *models.User + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (*models.User, error)); ok { + return rf(ctx, login) + } + if rf, ok := ret.Get(0).(func(context.Context, string) *models.User); ok { + r0 = rf(ctx, login) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.User) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, login) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +type mockConstructorTestingTNewUserRepo interface { + mock.TestingT + Cleanup(func()) +} + +// NewUserRepo creates a new instance of UserRepo. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewUserRepo(t mockConstructorTestingTNewUserRepo) *UserRepo { + mock := &UserRepo{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/storage/storage.go b/internal/storage/storage.go index f92501d..3eba614 100644 --- a/internal/storage/storage.go +++ b/internal/storage/storage.go @@ -4,15 +4,33 @@ import ( "context" "fmt" "github.com/vindosVP/loyalty-system/internal/models" - "github.com/vindosVP/loyalty-system/internal/repos" ) +//go:generate go run github.com/vektra/mockery/v2@v2.28.2 --name=UserRepo +type UserRepo interface { + Create(ctx context.Context, user *models.User) (*models.User, error) + GetByLogin(ctx context.Context, login string) (*models.User, error) + GetByID(ctx context.Context, id int) (*models.User, error) + Exists(ctx context.Context, login string) (bool, error) +} + +//go:generate go run github.com/vektra/mockery/v2@v2.28.2 --name=OrderRepo +type OrderRepo interface { + Create(ctx context.Context, order *models.Order) (*models.Order, error) + GetByID(ctx context.Context, id int) (*models.Order, error) + Exists(ctx context.Context, id int) (bool, error) + GetUsersOrders(ctx context.Context, userID int) ([]*models.Order, error) + GetUsersCurrentBalance(ctx context.Context, userID int) (float64, error) + GetUsersWithdrawnBalance(ctx context.Context, userID int) (float64, error) + GetUsersWithdrawals(ctx context.Context, userID int) ([]*models.Order, error) +} + type Storage struct { - userRepo *repos.UserRepo - orderRepo *repos.OrdersRepo + userRepo UserRepo + orderRepo OrderRepo } -func New(ur *repos.UserRepo, or *repos.OrdersRepo) *Storage { +func New(ur UserRepo, or OrderRepo) *Storage { return &Storage{userRepo: ur, orderRepo: or} } diff --git a/internal/storage/storage_test.go b/internal/storage/storage_test.go new file mode 100644 index 0000000..8b84c50 --- /dev/null +++ b/internal/storage/storage_test.go @@ -0,0 +1,963 @@ +package storage + +import ( + "context" + "errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/vindosVP/loyalty-system/internal/models" + "github.com/vindosVP/loyalty-system/internal/storage/mocks" + "testing" + "time" +) + +func TestStorage_CreateUser(t *testing.T) { + unexpectedError := errors.New("unexpected error") + + type userRepoExistsMock struct { + needed bool + result bool + err error + } + type userRepoCreateMock struct { + needed bool + result *models.User + err error + } + type args struct { + user *models.User + } + type want struct { + result *models.User + err error + } + + tests := []struct { + name string + userRepoExistsMock userRepoExistsMock + userRepoCreateMock userRepoCreateMock + args args + want want + }{ + { + name: "ok", + userRepoExistsMock: userRepoExistsMock{ + needed: true, + result: false, + err: nil, + }, + userRepoCreateMock: userRepoCreateMock{ + needed: true, + result: &models.User{ + ID: 1, + Login: "testUser", + EncryptedPwd: "encryptedPwd", + }, + err: nil, + }, + args: args{ + user: &models.User{ + ID: 1, + Login: "testUser", + EncryptedPwd: "encryptedPwd", + }, + }, + want: want{ + result: &models.User{ + ID: 1, + Login: "testUser", + EncryptedPwd: "encryptedPwd", + }, + err: nil, + }, + }, + { + name: "user already exists", + userRepoExistsMock: userRepoExistsMock{ + needed: true, + result: true, + err: nil, + }, + userRepoCreateMock: userRepoCreateMock{ + needed: false, + result: nil, + err: nil, + }, + args: args{ + user: &models.User{ + ID: 1, + Login: "testUser", + EncryptedPwd: "encryptedPwd", + }, + }, + want: want{ + result: nil, + err: ErrUserAlreadyExists, + }, + }, + { + name: "user exists unexpected error", + userRepoExistsMock: userRepoExistsMock{ + needed: true, + result: false, + err: unexpectedError, + }, + userRepoCreateMock: userRepoCreateMock{ + needed: false, + result: nil, + err: nil, + }, + args: args{ + user: &models.User{ + ID: 1, + Login: "testUser", + EncryptedPwd: "encryptedPwd", + }, + }, + want: want{ + result: nil, + err: unexpectedError, + }, + }, + { + name: "creation unexpected error", + userRepoExistsMock: userRepoExistsMock{ + needed: true, + result: false, + err: nil, + }, + userRepoCreateMock: userRepoCreateMock{ + needed: true, + result: nil, + err: unexpectedError, + }, + args: args{ + user: &models.User{ + ID: 1, + Login: "testUser", + EncryptedPwd: "encryptedPwd", + }, + }, + want: want{ + result: nil, + err: unexpectedError, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + userRepo := mocks.NewUserRepo(t) + orderRepo := mocks.NewOrderRepo(t) + s := New(userRepo, orderRepo) + + if tt.userRepoExistsMock.needed { + userRepo.On("Exists", mock.Anything, tt.args.user.Login).Return(tt.userRepoExistsMock.result, tt.userRepoExistsMock.err) + } + if tt.userRepoCreateMock.needed { + userRepo.On("Create", mock.Anything, tt.args.user).Return(tt.userRepoCreateMock.result, tt.userRepoCreateMock.err) + } + + result, err := s.CreateUser(ctx, tt.args.user) + + if tt.want.result != nil { + assert.Equal(t, tt.want.result, result) + assert.NoError(t, err) + } + + if tt.want.err != nil { + assert.ErrorIs(t, err, tt.want.err) + } + }) + } +} + +func TestStorage_GetUserByLogin(t *testing.T) { + unexpectedError := errors.New("unexpected error") + + type userRepoExistsMock struct { + needed bool + result bool + err error + } + type userRepoGetByLoginMock struct { + needed bool + result *models.User + err error + } + type args struct { + login string + } + type want struct { + result *models.User + err error + } + + tests := []struct { + name string + userRepoExistsMock userRepoExistsMock + userRepoGetByLoginMock userRepoGetByLoginMock + args args + want want + }{ + { + name: "ok", + userRepoExistsMock: userRepoExistsMock{ + needed: true, + result: true, + err: nil, + }, + userRepoGetByLoginMock: userRepoGetByLoginMock{ + needed: true, + result: &models.User{ + ID: 1, + Login: "testUser", + EncryptedPwd: "encryptedPwd", + }, + err: nil, + }, + args: args{ + login: "testUser", + }, + want: want{ + result: &models.User{ + ID: 1, + Login: "testUser", + EncryptedPwd: "encryptedPwd", + }, + err: nil, + }, + }, + { + name: "user not found", + userRepoExistsMock: userRepoExistsMock{ + needed: true, + result: false, + err: nil, + }, + userRepoGetByLoginMock: userRepoGetByLoginMock{ + needed: false, + result: nil, + err: nil, + }, + args: args{ + login: "testUser", + }, + want: want{ + result: nil, + err: ErrUserNotFound, + }, + }, + { + name: "user exists unexpected error", + userRepoExistsMock: userRepoExistsMock{ + needed: true, + result: false, + err: unexpectedError, + }, + userRepoGetByLoginMock: userRepoGetByLoginMock{ + needed: false, + result: nil, + err: nil, + }, + args: args{ + login: "testUser", + }, + want: want{ + result: nil, + err: unexpectedError, + }, + }, + { + name: "userRepo.GetByLogin unexpected error", + userRepoExistsMock: userRepoExistsMock{ + needed: true, + result: true, + err: nil, + }, + userRepoGetByLoginMock: userRepoGetByLoginMock{ + needed: true, + result: nil, + err: unexpectedError, + }, + args: args{ + login: "testUser", + }, + want: want{ + result: nil, + err: unexpectedError, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + userRepo := mocks.NewUserRepo(t) + orderRepo := mocks.NewOrderRepo(t) + s := New(userRepo, orderRepo) + + if tt.userRepoExistsMock.needed { + userRepo.On("Exists", mock.Anything, tt.args.login).Return(tt.userRepoExistsMock.result, tt.userRepoExistsMock.err) + } + if tt.userRepoGetByLoginMock.needed { + userRepo.On("GetByLogin", mock.Anything, tt.args.login).Return(tt.userRepoGetByLoginMock.result, tt.userRepoGetByLoginMock.err) + } + + result, err := s.GetUserByLogin(ctx, tt.args.login) + + if tt.want.result != nil { + assert.Equal(t, tt.want.result, result) + assert.NoError(t, err) + } + + if tt.want.err != nil { + assert.ErrorIs(t, err, tt.want.err) + } + }) + } +} + +func TestStorage_CreateOrder(t *testing.T) { + unexpectedError := errors.New("unexpected error") + currentTime := time.Now() + + type OrderRepoExistsMock struct { + needed bool + result bool + err error + } + type OrderRepoGetByIDMock struct { + needed bool + result *models.Order + err error + } + type OrderRepoCreateMock struct { + needed bool + result *models.Order + err error + } + type args struct { + order *models.Order + } + type want struct { + result *models.Order + err error + } + + tests := []struct { + name string + orderRepoExistsMock OrderRepoExistsMock + orderRepoGetByIDMock OrderRepoGetByIDMock + orderRepoCreateMock OrderRepoCreateMock + args args + want want + }{ + { + name: "ok", + orderRepoExistsMock: OrderRepoExistsMock{ + needed: true, + result: false, + err: nil, + }, + orderRepoGetByIDMock: OrderRepoGetByIDMock{ + needed: false, + result: nil, + err: nil, + }, + orderRepoCreateMock: OrderRepoCreateMock{ + needed: true, + result: &models.Order{ + ID: 1, + UserID: 1, + Status: models.OrderStatusNew, + Sum: 0, + UploadedAt: currentTime, + }, + err: nil, + }, + args: args{ + order: &models.Order{ + ID: 1, + UserID: 1, + Status: models.OrderStatusNew, + Sum: 0, + UploadedAt: currentTime, + }, + }, + want: want{ + result: &models.Order{ + ID: 1, + UserID: 1, + Status: models.OrderStatusNew, + Sum: 0, + UploadedAt: currentTime, + }, + err: nil, + }, + }, + { + name: "order already exists", + orderRepoExistsMock: OrderRepoExistsMock{ + needed: true, + result: true, + err: nil, + }, + orderRepoGetByIDMock: OrderRepoGetByIDMock{ + needed: true, + result: &models.Order{ + ID: 1, + UserID: 1, + Status: models.OrderStatusNew, + Sum: 0, + UploadedAt: currentTime, + }, + err: nil, + }, + orderRepoCreateMock: OrderRepoCreateMock{ + needed: false, + result: nil, + err: nil, + }, + args: args{ + order: &models.Order{ + ID: 1, + UserID: 1, + Status: models.OrderStatusNew, + Sum: 0, + UploadedAt: currentTime, + }, + }, + want: want{ + result: nil, + err: ErrOrderAlreadyExists, + }, + }, + { + name: "order already created by other user", + orderRepoExistsMock: OrderRepoExistsMock{ + needed: true, + result: true, + err: nil, + }, + orderRepoGetByIDMock: OrderRepoGetByIDMock{ + needed: true, + result: &models.Order{ + ID: 1, + UserID: 2, + Status: models.OrderStatusNew, + Sum: 0, + UploadedAt: currentTime, + }, + err: nil, + }, + orderRepoCreateMock: OrderRepoCreateMock{ + needed: false, + result: nil, + err: nil, + }, + args: args{ + order: &models.Order{ + ID: 1, + UserID: 1, + Status: models.OrderStatusNew, + Sum: 0, + UploadedAt: currentTime, + }, + }, + want: want{ + result: nil, + err: ErrOrderCreatedByOtherUser, + }, + }, + { + name: "orderRepo.Exists unexpected error", + orderRepoExistsMock: OrderRepoExistsMock{ + needed: true, + result: false, + err: unexpectedError, + }, + orderRepoGetByIDMock: OrderRepoGetByIDMock{ + needed: false, + result: nil, + err: nil, + }, + orderRepoCreateMock: OrderRepoCreateMock{ + needed: false, + result: nil, + err: nil, + }, + args: args{ + order: &models.Order{ + ID: 1, + UserID: 1, + Status: models.OrderStatusNew, + Sum: 0, + UploadedAt: currentTime, + }, + }, + want: want{ + result: nil, + err: unexpectedError, + }, + }, + { + name: "orderRepo.GetByID unexpected error", + orderRepoExistsMock: OrderRepoExistsMock{ + needed: true, + result: true, + err: nil, + }, + orderRepoGetByIDMock: OrderRepoGetByIDMock{ + needed: true, + result: nil, + err: unexpectedError, + }, + orderRepoCreateMock: OrderRepoCreateMock{ + needed: false, + result: nil, + err: nil, + }, + args: args{ + order: &models.Order{ + ID: 1, + UserID: 1, + Status: models.OrderStatusNew, + Sum: 0, + UploadedAt: currentTime, + }, + }, + want: want{ + result: nil, + err: unexpectedError, + }, + }, + { + name: "orderRepo.Create unexpected error", + orderRepoExistsMock: OrderRepoExistsMock{ + needed: true, + result: false, + err: nil, + }, + orderRepoGetByIDMock: OrderRepoGetByIDMock{ + needed: false, + result: nil, + err: nil, + }, + orderRepoCreateMock: OrderRepoCreateMock{ + needed: true, + result: nil, + err: unexpectedError, + }, + args: args{ + order: &models.Order{ + ID: 1, + UserID: 1, + Status: models.OrderStatusNew, + Sum: 0, + UploadedAt: currentTime, + }, + }, + want: want{ + result: nil, + err: unexpectedError, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + userRepo := mocks.NewUserRepo(t) + orderRepo := mocks.NewOrderRepo(t) + s := New(userRepo, orderRepo) + if tt.orderRepoExistsMock.needed { + orderRepo.On("Exists", mock.Anything, tt.args.order.ID).Return(tt.orderRepoExistsMock.result, tt.orderRepoExistsMock.err) + } + if tt.orderRepoGetByIDMock.needed { + orderRepo.On("GetByID", mock.Anything, tt.args.order.ID).Return(tt.orderRepoGetByIDMock.result, tt.orderRepoGetByIDMock.err) + } + if tt.orderRepoCreateMock.needed { + orderRepo.On("Create", mock.Anything, tt.args.order).Return(tt.orderRepoCreateMock.result, tt.orderRepoCreateMock.err) + } + + result, err := s.CreateOrder(ctx, tt.args.order) + if tt.want.result != nil { + assert.Equal(t, tt.want.result, result) + assert.NoError(t, err) + } + if tt.want.err != nil { + assert.ErrorIs(t, err, tt.want.err) + } + }) + } +} + +func TestStorage_GetUsersOrders(t *testing.T) { + unexpectedError := errors.New("unexpected error") + currentTime := time.Now() + + type orderRepoGetUsersOrdersMock struct { + needed bool + result []*models.Order + err error + } + type args struct { + userID int + } + type want struct { + result []*models.Order + err error + } + + tests := []struct { + name string + orderRepoGetUsersOrdersMock orderRepoGetUsersOrdersMock + args args + want want + }{ + { + name: "ok", + orderRepoGetUsersOrdersMock: orderRepoGetUsersOrdersMock{ + needed: true, + result: []*models.Order{ + { + ID: 1, + UserID: 1, + Status: models.OrderStatusNew, + Sum: 0, + UploadedAt: currentTime, + }, + { + ID: 2, + UserID: 1, + Status: models.OrderStatusNew, + Sum: 0, + UploadedAt: currentTime, + }, + }, + err: nil, + }, + args: args{ + userID: 1, + }, + want: want{ + result: []*models.Order{ + { + ID: 1, + UserID: 1, + Status: models.OrderStatusNew, + Sum: 0, + UploadedAt: currentTime, + }, + { + ID: 2, + UserID: 1, + Status: models.OrderStatusNew, + Sum: 0, + UploadedAt: currentTime, + }, + }, + err: nil, + }, + }, + { + name: "orderRepo.GetUsersOrders unexpected error", + orderRepoGetUsersOrdersMock: orderRepoGetUsersOrdersMock{ + needed: true, + result: nil, + err: unexpectedError, + }, + args: args{ + userID: 1, + }, + want: want{ + result: nil, + err: unexpectedError, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + userRepo := mocks.NewUserRepo(t) + orderRepo := mocks.NewOrderRepo(t) + s := New(userRepo, orderRepo) + + if tt.orderRepoGetUsersOrdersMock.needed { + orderRepo.On("GetUsersOrders", mock.Anything, tt.args.userID).Return(tt.orderRepoGetUsersOrdersMock.result, tt.orderRepoGetUsersOrdersMock.err) + } + + result, err := s.GetUsersOrders(ctx, tt.args.userID) + if tt.want.result != nil { + assert.Equal(t, tt.want.result, result) + assert.NoError(t, err) + } + if tt.want.err != nil { + assert.ErrorIs(t, err, tt.want.err) + } + }) + } +} + +func TestStorage_GetUsersCurrentBalance(t *testing.T) { + unexpectedError := errors.New("unexpected error") + + type orderRepoGetUsersCurrentBalanceMock struct { + needed bool + result float64 + err error + } + type args struct { + userID int + } + type want struct { + result float64 + err error + } + + tests := []struct { + name string + orderRepoGetUsersCurrentBalanceMock orderRepoGetUsersCurrentBalanceMock + args args + want want + }{ + { + name: "ok", + orderRepoGetUsersCurrentBalanceMock: orderRepoGetUsersCurrentBalanceMock{ + needed: true, + result: 512.3, + err: nil, + }, + args: args{ + userID: 1, + }, + want: want{ + result: 512.3, + err: nil, + }, + }, + { + name: "orderRepo.GetUsersCurrentBalance unexpected error", + orderRepoGetUsersCurrentBalanceMock: orderRepoGetUsersCurrentBalanceMock{ + needed: true, + result: 0, + err: unexpectedError, + }, + args: args{ + userID: 1, + }, + want: want{ + result: 0, + err: unexpectedError, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + userRepo := mocks.NewUserRepo(t) + orderRepo := mocks.NewOrderRepo(t) + s := New(userRepo, orderRepo) + + if tt.orderRepoGetUsersCurrentBalanceMock.needed { + orderRepo.On("GetUsersCurrentBalance", mock.Anything, tt.args.userID).Return(tt.orderRepoGetUsersCurrentBalanceMock.result, tt.orderRepoGetUsersCurrentBalanceMock.err) + } + + result, err := s.GetUsersCurrentBalance(ctx, tt.args.userID) + if tt.want.err == nil { + assert.Equal(t, tt.want.result, result) + assert.NoError(t, err) + } else { + assert.ErrorIs(t, err, tt.want.err) + } + }) + } +} + +func TestStorage_GetUsersWithdrawnBalance(t *testing.T) { + unexpectedError := errors.New("unexpected error") + + type orderRepoGetUsersWithdrawnBalanceMock struct { + needed bool + result float64 + err error + } + type args struct { + userID int + } + type want struct { + result float64 + err error + } + + tests := []struct { + name string + orderRepoGetUsersWithdrawnBalanceMock orderRepoGetUsersWithdrawnBalanceMock + args args + want want + }{ + { + name: "ok", + orderRepoGetUsersWithdrawnBalanceMock: orderRepoGetUsersWithdrawnBalanceMock{ + needed: true, + result: 512.3, + err: nil, + }, + args: args{ + userID: 1, + }, + want: want{ + result: 512.3, + err: nil, + }, + }, + { + name: "orderRepo.GetUsersWithdrawnBalance unexpected error", + orderRepoGetUsersWithdrawnBalanceMock: orderRepoGetUsersWithdrawnBalanceMock{ + needed: true, + result: 0, + err: unexpectedError, + }, + args: args{ + userID: 1, + }, + want: want{ + result: 0, + err: unexpectedError, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + userRepo := mocks.NewUserRepo(t) + orderRepo := mocks.NewOrderRepo(t) + s := New(userRepo, orderRepo) + + if tt.orderRepoGetUsersWithdrawnBalanceMock.needed { + orderRepo.On("GetUsersWithdrawnBalance", mock.Anything, tt.args.userID).Return(tt.orderRepoGetUsersWithdrawnBalanceMock.result, tt.orderRepoGetUsersWithdrawnBalanceMock.err) + } + + result, err := s.GetUsersWithdrawnBalance(ctx, tt.args.userID) + if tt.want.err == nil { + assert.Equal(t, tt.want.result, result) + assert.NoError(t, err) + } else { + assert.ErrorIs(t, err, tt.want.err) + } + }) + } +} + +func TestStorage_GetUsersWithdrawals(t *testing.T) { + unexpectedError := errors.New("unexpected error") + currentTime := time.Now() + + type orderRepoGetUsersWithdrawalsMock struct { + needed bool + result []*models.Order + err error + } + type args struct { + userID int + } + type want struct { + result []*models.Order + err error + } + + tests := []struct { + name string + orderRepoGetUsersWithdrawalsMock orderRepoGetUsersWithdrawalsMock + args args + want want + }{ + { + name: "ok", + orderRepoGetUsersWithdrawalsMock: orderRepoGetUsersWithdrawalsMock{ + needed: true, + result: []*models.Order{ + { + ID: 1, + UserID: 1, + Status: models.OrderStatusProcessed, + Sum: 100, + UploadedAt: currentTime, + }, + { + ID: 2, + UserID: 1, + Status: models.OrderStatusProcessed, + Sum: 200, + UploadedAt: currentTime, + }, + }, + err: nil, + }, + args: args{ + userID: 1, + }, + want: want{ + result: []*models.Order{ + { + ID: 1, + UserID: 1, + Status: models.OrderStatusProcessed, + Sum: 100, + UploadedAt: currentTime, + }, + { + ID: 2, + UserID: 1, + Status: models.OrderStatusProcessed, + Sum: 200, + UploadedAt: currentTime, + }, + }, + err: nil, + }, + }, + { + name: "orderRepo.GetUsersWithdrawals unexpected error", + orderRepoGetUsersWithdrawalsMock: orderRepoGetUsersWithdrawalsMock{ + needed: true, + result: nil, + err: unexpectedError, + }, + args: args{ + userID: 1, + }, + want: want{ + result: nil, + err: unexpectedError, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + userRepo := mocks.NewUserRepo(t) + orderRepo := mocks.NewOrderRepo(t) + s := New(userRepo, orderRepo) + + if tt.orderRepoGetUsersWithdrawalsMock.needed { + orderRepo.On("GetUsersWithdrawals", mock.Anything, tt.args.userID).Return(tt.orderRepoGetUsersWithdrawalsMock.result, tt.orderRepoGetUsersWithdrawalsMock.err) + } + + result, err := s.GetUsersWithdrawals(ctx, tt.args.userID) + if tt.want.result != nil { + assert.Equal(t, tt.want.result, result) + assert.NoError(t, err) + } + if tt.want.err != nil { + assert.ErrorIs(t, err, tt.want.err) + } + }) + } +} diff --git a/pkg/auth/auth.go b/pkg/auth/auth.go index af05868..176602d 100644 --- a/pkg/auth/auth.go +++ b/pkg/auth/auth.go @@ -7,8 +7,8 @@ import ( ) var ( - errNoAuthHeader = fmt.Errorf("no auth header") - errInvalidAuthFormat = fmt.Errorf("invalid auth format") + ErrNoAuthHeader = fmt.Errorf("no auth header") + ErrInvalidAuthFormat = fmt.Errorf("invalid auth format") ) const bearerSchema = "Bearer " @@ -16,11 +16,11 @@ const bearerSchema = "Bearer " func ParseBearerToken(r *http.Request) (string, error) { reqToken := r.Header.Get("Authorization") if reqToken == "" { - return "", errNoAuthHeader + return "", ErrNoAuthHeader } splitToken := strings.Split(reqToken, bearerSchema) if len(splitToken) != 2 { - return "", errInvalidAuthFormat + return "", ErrInvalidAuthFormat } return splitToken[1], nil } diff --git a/pkg/auth/auth_test.go b/pkg/auth/auth_test.go new file mode 100644 index 0000000..b4cfe3e --- /dev/null +++ b/pkg/auth/auth_test.go @@ -0,0 +1,76 @@ +package auth + +import ( + "github.com/stretchr/testify/assert" + "net/http" + "testing" +) + +func TestParseBearerToken(t *testing.T) { + type args struct { + r *http.Request + } + type want struct { + token string + err error + } + + tests := []struct { + name string + args args + want want + }{ + { + name: "ok", + args: args{ + r: &http.Request{ + Header: http.Header{ + "Authorization": []string{"Bearer someToken"}, + }, + }, + }, + want: want{ + token: "someToken", + err: nil, + }, + }, + { + name: "no auth header", + args: args{ + r: &http.Request{ + Header: http.Header{}, + }, + }, + want: want{ + token: "", + err: ErrNoAuthHeader, + }, + }, + { + name: "invalid auth format", + args: args{ + r: &http.Request{ + Header: http.Header{ + "Authorization": []string{"Basic someToken"}, + }, + }, + }, + want: want{ + token: "", + err: ErrInvalidAuthFormat, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + token, err := ParseBearerToken(tt.args.r) + if tt.want.err == nil { + assert.Equal(t, tt.want.token, token) + assert.NoError(t, err) + } else { + assert.ErrorIs(t, err, tt.want.err) + } + }) + } +} diff --git a/pkg/passwords/passwords_test.go b/pkg/passwords/passwords_test.go new file mode 100644 index 0000000..d2db577 --- /dev/null +++ b/pkg/passwords/passwords_test.go @@ -0,0 +1,18 @@ +package passwords + +import ( + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "testing" +) + +func TestCompare(t *testing.T) { + pwd := "somePassword" + + encrypted, err := Encrypt(pwd) + require.NoError(t, err) + require.NotEmpty(t, encrypted) + + valid := Compare(pwd, encrypted) + assert.True(t, valid) +} diff --git a/pkg/tokens/jwt_test.go b/pkg/tokens/jwt_test.go new file mode 100644 index 0000000..9a47118 --- /dev/null +++ b/pkg/tokens/jwt_test.go @@ -0,0 +1,24 @@ +package tokens + +import ( + "github.com/stretchr/testify/assert" + "strconv" + "testing" + "time" +) + +func TestExtractID(t *testing.T) { + userID := 1 + userLogin := "someLogin" + jwtSecret := "superSecret" + + token, err := CreateJWT(JWTClaims(userID, userLogin, time.Now().Add(time.Hour*72).Unix()), jwtSecret) + assert.NoError(t, err) + + strID, err := ExtractID(token, jwtSecret) + assert.NoError(t, err) + + id, err := strconv.Atoi(strID) + assert.NoError(t, err) + assert.Equal(t, userID, id) +}