From 2c000e0585040b8143769c0bc7fcf7a65b9eb3de Mon Sep 17 00:00:00 2001 From: Polina Sheviakova Date: Fri, 26 Jul 2024 17:03:37 +0300 Subject: [PATCH] Add reading from cache --- go.mod | 3 +- go.sum | 6 +- internal/app/service_provider.go | 27 +- internal/model/errors.go | 5 + internal/repository/cache/converter/cache.go | 29 + internal/repository/cache/model/cache.go | 11 + internal/repository/cache/repository.go | 69 ++ internal/repository/generate.go | 1 + .../mocks/cache_repository_minimock.go | 725 ++++++++++++++++++ internal/repository/repository.go | 6 + internal/service/auth/login.go | 2 +- internal/service/user/get.go | 22 +- internal/service/user/service.go | 16 +- 13 files changed, 902 insertions(+), 20 deletions(-) create mode 100644 internal/model/errors.go create mode 100644 internal/repository/cache/converter/cache.go create mode 100644 internal/repository/cache/model/cache.go create mode 100644 internal/repository/cache/repository.go create mode 100644 internal/repository/mocks/cache_repository_minimock.go diff --git a/go.mod b/go.mod index 5513d65..d43c225 100644 --- a/go.mod +++ b/go.mod @@ -8,13 +8,14 @@ require ( github.com/gojuno/minimock/v3 v3.3.13 github.com/golang-jwt/jwt/v5 v5.2.1 github.com/golang/protobuf v1.5.4 + github.com/gomodule/redigo v1.9.2 github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 github.com/jackc/pgx/v5 v5.6.0 github.com/joho/godotenv v1.5.1 github.com/natefinch/lumberjack v2.0.0+incompatible github.com/pkg/errors v0.9.1 - github.com/polshe-v/microservices_common v0.0.0-20240614134025-0c890c19055d + github.com/polshe-v/microservices_common v0.0.0-20240725124217-0fcebed7fef8 github.com/prometheus/client_golang v1.19.1 github.com/rakyll/statik v0.1.7 github.com/rs/cors v1.11.0 diff --git a/go.sum b/go.sum index 92afb94..fe51713 100644 --- a/go.sum +++ b/go.sum @@ -30,6 +30,8 @@ github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17w github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/gomodule/redigo v1.9.2 h1:HrutZBLhSIU8abiSfW8pj8mPhOyMYjZT/wcA4/L9L9s= +github.com/gomodule/redigo v1.9.2/go.mod h1:KsU3hiK/Ay8U42qpaJk+kuNa3C+spxapWpM+ywhcgtw= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -66,8 +68,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/polshe-v/microservices_common v0.0.0-20240614134025-0c890c19055d h1:QQT0ZAHq5cmY3ExIzSwkb+pW5pLiayAostC2TO2/yfA= -github.com/polshe-v/microservices_common v0.0.0-20240614134025-0c890c19055d/go.mod h1:GXrHUWaU8yp9SPhdv/wyXcYM4fyvvXZYN8gelJRKNxk= +github.com/polshe-v/microservices_common v0.0.0-20240725124217-0fcebed7fef8 h1:yyv+1RSy4CwRUN+myBih6t+eCn+hzbGJFCJhy2VUH1w= +github.com/polshe-v/microservices_common v0.0.0-20240725124217-0fcebed7fef8/go.mod h1:RVZzF43a2rjqcgx2zz378b/dPHYCwSqY5czSW4ClbrU= github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= diff --git a/internal/app/service_provider.go b/internal/app/service_provider.go index d3c5c31..1f59cf0 100644 --- a/internal/app/service_provider.go +++ b/internal/app/service_provider.go @@ -13,6 +13,7 @@ import ( "github.com/polshe-v/microservices_auth/internal/config/env" "github.com/polshe-v/microservices_auth/internal/repository" accessRepository "github.com/polshe-v/microservices_auth/internal/repository/access" + cacheRepository "github.com/polshe-v/microservices_auth/internal/repository/cache" keyRepository "github.com/polshe-v/microservices_auth/internal/repository/key" logRepository "github.com/polshe-v/microservices_auth/internal/repository/log" userRepository "github.com/polshe-v/microservices_auth/internal/repository/user" @@ -51,12 +52,15 @@ type serviceProvider struct { keyRepository repository.KeyRepository accessRepository repository.AccessRepository logRepository repository.LogRepository - userService service.UserService - authService service.AuthService - accessService service.AccessService - userImpl *user.Implementation - authImpl *auth.Implementation - accessImpl *access.Implementation + cacheRepository repository.CacheRepository + + userService service.UserService + authService service.AuthService + accessService service.AccessService + + userImpl *user.Implementation + authImpl *auth.Implementation + accessImpl *access.Implementation tokenOperations tokens.TokenOperations } @@ -196,7 +200,7 @@ func (s *serviceProvider) TxManager(ctx context.Context) db.TxManager { return s.txManager } -func (s *serviceProvider) RedisClient() cache.RedisClient { +func (s *serviceProvider) RedisClient() cache.Client { if s.redisClient == nil { s.redisClient = redis.NewClient(s.RedisPool(), s.RedisConfig().ConnectionTimeout()) } @@ -246,9 +250,16 @@ func (s *serviceProvider) LogRepository(ctx context.Context) repository.LogRepos return s.logRepository } +func (s *serviceProvider) CacheRepository(ctx context.Context) repository.CacheRepository { + if s.cacheRepository == nil { + s.cacheRepository = cacheRepository.NewRepository(s.RedisClient()) + } + return s.cacheRepository +} + func (s *serviceProvider) UserService(ctx context.Context) service.UserService { if s.userService == nil { - s.userService = userService.NewService(s.UserRepository(ctx), s.LogRepository(ctx), s.TxManager(ctx)) + s.userService = userService.NewService(s.UserRepository(ctx), s.CacheRepository(ctx), s.LogRepository(ctx), s.TxManager(ctx)) } return s.userService } diff --git a/internal/model/errors.go b/internal/model/errors.go new file mode 100644 index 0000000..7bb2661 --- /dev/null +++ b/internal/model/errors.go @@ -0,0 +1,5 @@ +package model + +import "github.com/pkg/errors" + +var ErrorUserNotFound = errors.New("user not found") diff --git a/internal/repository/cache/converter/cache.go b/internal/repository/cache/converter/cache.go new file mode 100644 index 0000000..de86be7 --- /dev/null +++ b/internal/repository/cache/converter/cache.go @@ -0,0 +1,29 @@ +package converter + +import ( + "database/sql" + "time" + + model "github.com/polshe-v/microservices_auth/internal/model" + modelRepo "github.com/polshe-v/microservices_auth/internal/repository/cache/model" +) + +// ToUserFromRepo converts repository layer model to structure of service layer. +func ToUserFromRepo(user *modelRepo.User) *model.User { + var updatedAt sql.NullTime + if user.UpdatedAtNs != nil { + updatedAt = sql.NullTime{ + Time: time.Unix(0, *user.UpdatedAtNs), + Valid: true, + } + } + + return &model.User{ + ID: user.ID, + Name: user.Name, + Email: user.Email, + Role: user.Role, + CreatedAt: time.Unix(0, user.CreatedAtNs), + UpdatedAt: updatedAt, + } +} diff --git a/internal/repository/cache/model/cache.go b/internal/repository/cache/model/cache.go new file mode 100644 index 0000000..0e27698 --- /dev/null +++ b/internal/repository/cache/model/cache.go @@ -0,0 +1,11 @@ +package model + +// User type is the main structure for user. +type User struct { + ID int64 `redis:"id"` + Name string `redis:"name"` + Email string `redis:"email"` + Role string `redis:"role"` + CreatedAtNs int64 `redis:"created_at"` + UpdatedAtNs *int64 `redis:"updated_at"` +} diff --git a/internal/repository/cache/repository.go b/internal/repository/cache/repository.go new file mode 100644 index 0000000..92bad71 --- /dev/null +++ b/internal/repository/cache/repository.go @@ -0,0 +1,69 @@ +package cache + +import ( + "context" + "strconv" + + redigo "github.com/gomodule/redigo/redis" + + "github.com/polshe-v/microservices_auth/internal/model" + "github.com/polshe-v/microservices_auth/internal/repository" + "github.com/polshe-v/microservices_auth/internal/repository/cache/converter" + modelRepo "github.com/polshe-v/microservices_auth/internal/repository/cache/model" + "github.com/polshe-v/microservices_common/pkg/cache" +) + +type repo struct { + client cache.Client +} + +// NewRepository creates new object of repository layer. +func NewRepository(client cache.Client) repository.CacheRepository { + return &repo{client: client} +} + +func (r *repo) CreateRecord(ctx context.Context, user *model.User) error { + var updatedAtNs *int64 + + if user.UpdatedAt.Valid { + updatedAt := user.UpdatedAt.Time.UnixNano() + updatedAtNs = &updatedAt + } + + userRecord := modelRepo.User{ + ID: user.ID, + Name: user.Name, + Email: user.Email, + Role: user.Role, + CreatedAtNs: user.CreatedAt.UnixNano(), + UpdatedAtNs: updatedAtNs, + } + + idStr := strconv.FormatInt(user.ID, 10) + err := r.client.HSet(ctx, idStr, userRecord) + if err != nil { + return err + } + + return nil +} + +func (r *repo) GetRecord(ctx context.Context, id int64) (*model.User, error) { + idStr := strconv.FormatInt(id, 10) + values, err := r.client.HGetAll(ctx, idStr) + if err != nil { + return nil, err + } + + if len(values) == 0 { + return nil, model.ErrorUserNotFound + } + + var user modelRepo.User + err = redigo.ScanStruct(values, &user) + if err != nil { + return nil, err + } + + return converter.ToUserFromRepo(&user), nil +} diff --git a/internal/repository/generate.go b/internal/repository/generate.go index f3be757..92c709f 100644 --- a/internal/repository/generate.go +++ b/internal/repository/generate.go @@ -5,3 +5,4 @@ package repository //go:generate ./../../bin/minimock -g -i KeyRepository -o ./mocks/ -s "_minimock.go" //go:generate ./../../bin/minimock -g -i AccessRepository -o ./mocks/ -s "_minimock.go" //go:generate ./../../bin/minimock -g -i LogRepository -o ./mocks/ -s "_minimock.go" +//go:generate ./../../bin/minimock -g -i CacheRepository -o ./mocks/ -s "_minimock.go" diff --git a/internal/repository/mocks/cache_repository_minimock.go b/internal/repository/mocks/cache_repository_minimock.go new file mode 100644 index 0000000..2dc2b6e --- /dev/null +++ b/internal/repository/mocks/cache_repository_minimock.go @@ -0,0 +1,725 @@ +// Code generated by http://github.com/gojuno/minimock (v3.3.12). DO NOT EDIT. + +package mocks + +import ( + "context" + "sync" + mm_atomic "sync/atomic" + mm_time "time" + + "github.com/gojuno/minimock/v3" + "github.com/polshe-v/microservices_auth/internal/model" +) + +// CacheRepositoryMock implements repository.CacheRepository +type CacheRepositoryMock struct { + t minimock.Tester + finishOnce sync.Once + + funcCreateRecord func(ctx context.Context, user *model.User) (err error) + inspectFuncCreateRecord func(ctx context.Context, user *model.User) + afterCreateRecordCounter uint64 + beforeCreateRecordCounter uint64 + CreateRecordMock mCacheRepositoryMockCreateRecord + + funcGetRecord func(ctx context.Context, id int64) (up1 *model.User, err error) + inspectFuncGetRecord func(ctx context.Context, id int64) + afterGetRecordCounter uint64 + beforeGetRecordCounter uint64 + GetRecordMock mCacheRepositoryMockGetRecord +} + +// NewCacheRepositoryMock returns a mock for repository.CacheRepository +func NewCacheRepositoryMock(t minimock.Tester) *CacheRepositoryMock { + m := &CacheRepositoryMock{t: t} + + if controller, ok := t.(minimock.MockController); ok { + controller.RegisterMocker(m) + } + + m.CreateRecordMock = mCacheRepositoryMockCreateRecord{mock: m} + m.CreateRecordMock.callArgs = []*CacheRepositoryMockCreateRecordParams{} + + m.GetRecordMock = mCacheRepositoryMockGetRecord{mock: m} + m.GetRecordMock.callArgs = []*CacheRepositoryMockGetRecordParams{} + + t.Cleanup(m.MinimockFinish) + + return m +} + +type mCacheRepositoryMockCreateRecord struct { + optional bool + mock *CacheRepositoryMock + defaultExpectation *CacheRepositoryMockCreateRecordExpectation + expectations []*CacheRepositoryMockCreateRecordExpectation + + callArgs []*CacheRepositoryMockCreateRecordParams + mutex sync.RWMutex + + expectedInvocations uint64 +} + +// CacheRepositoryMockCreateRecordExpectation specifies expectation struct of the CacheRepository.CreateRecord +type CacheRepositoryMockCreateRecordExpectation struct { + mock *CacheRepositoryMock + params *CacheRepositoryMockCreateRecordParams + paramPtrs *CacheRepositoryMockCreateRecordParamPtrs + results *CacheRepositoryMockCreateRecordResults + Counter uint64 +} + +// CacheRepositoryMockCreateRecordParams contains parameters of the CacheRepository.CreateRecord +type CacheRepositoryMockCreateRecordParams struct { + ctx context.Context + user *model.User +} + +// CacheRepositoryMockCreateRecordParamPtrs contains pointers to parameters of the CacheRepository.CreateRecord +type CacheRepositoryMockCreateRecordParamPtrs struct { + ctx *context.Context + user **model.User +} + +// CacheRepositoryMockCreateRecordResults contains results of the CacheRepository.CreateRecord +type CacheRepositoryMockCreateRecordResults struct { + err error +} + +// Marks this method to be optional. The default behavior of any method with Return() is '1 or more', meaning +// the test will fail minimock's automatic final call check if the mocked method was not called at least once. +// Optional() makes method check to work in '0 or more' mode. +// It is NOT RECOMMENDED to use this option unless you really need it, as default behaviour helps to +// catch the problems when the expected method call is totally skipped during test run. +func (mmCreateRecord *mCacheRepositoryMockCreateRecord) Optional() *mCacheRepositoryMockCreateRecord { + mmCreateRecord.optional = true + return mmCreateRecord +} + +// Expect sets up expected params for CacheRepository.CreateRecord +func (mmCreateRecord *mCacheRepositoryMockCreateRecord) Expect(ctx context.Context, user *model.User) *mCacheRepositoryMockCreateRecord { + if mmCreateRecord.mock.funcCreateRecord != nil { + mmCreateRecord.mock.t.Fatalf("CacheRepositoryMock.CreateRecord mock is already set by Set") + } + + if mmCreateRecord.defaultExpectation == nil { + mmCreateRecord.defaultExpectation = &CacheRepositoryMockCreateRecordExpectation{} + } + + if mmCreateRecord.defaultExpectation.paramPtrs != nil { + mmCreateRecord.mock.t.Fatalf("CacheRepositoryMock.CreateRecord mock is already set by ExpectParams functions") + } + + mmCreateRecord.defaultExpectation.params = &CacheRepositoryMockCreateRecordParams{ctx, user} + for _, e := range mmCreateRecord.expectations { + if minimock.Equal(e.params, mmCreateRecord.defaultExpectation.params) { + mmCreateRecord.mock.t.Fatalf("Expectation set by When has same params: %#v", *mmCreateRecord.defaultExpectation.params) + } + } + + return mmCreateRecord +} + +// ExpectCtxParam1 sets up expected param ctx for CacheRepository.CreateRecord +func (mmCreateRecord *mCacheRepositoryMockCreateRecord) ExpectCtxParam1(ctx context.Context) *mCacheRepositoryMockCreateRecord { + if mmCreateRecord.mock.funcCreateRecord != nil { + mmCreateRecord.mock.t.Fatalf("CacheRepositoryMock.CreateRecord mock is already set by Set") + } + + if mmCreateRecord.defaultExpectation == nil { + mmCreateRecord.defaultExpectation = &CacheRepositoryMockCreateRecordExpectation{} + } + + if mmCreateRecord.defaultExpectation.params != nil { + mmCreateRecord.mock.t.Fatalf("CacheRepositoryMock.CreateRecord mock is already set by Expect") + } + + if mmCreateRecord.defaultExpectation.paramPtrs == nil { + mmCreateRecord.defaultExpectation.paramPtrs = &CacheRepositoryMockCreateRecordParamPtrs{} + } + mmCreateRecord.defaultExpectation.paramPtrs.ctx = &ctx + + return mmCreateRecord +} + +// ExpectUserParam2 sets up expected param user for CacheRepository.CreateRecord +func (mmCreateRecord *mCacheRepositoryMockCreateRecord) ExpectUserParam2(user *model.User) *mCacheRepositoryMockCreateRecord { + if mmCreateRecord.mock.funcCreateRecord != nil { + mmCreateRecord.mock.t.Fatalf("CacheRepositoryMock.CreateRecord mock is already set by Set") + } + + if mmCreateRecord.defaultExpectation == nil { + mmCreateRecord.defaultExpectation = &CacheRepositoryMockCreateRecordExpectation{} + } + + if mmCreateRecord.defaultExpectation.params != nil { + mmCreateRecord.mock.t.Fatalf("CacheRepositoryMock.CreateRecord mock is already set by Expect") + } + + if mmCreateRecord.defaultExpectation.paramPtrs == nil { + mmCreateRecord.defaultExpectation.paramPtrs = &CacheRepositoryMockCreateRecordParamPtrs{} + } + mmCreateRecord.defaultExpectation.paramPtrs.user = &user + + return mmCreateRecord +} + +// Inspect accepts an inspector function that has same arguments as the CacheRepository.CreateRecord +func (mmCreateRecord *mCacheRepositoryMockCreateRecord) Inspect(f func(ctx context.Context, user *model.User)) *mCacheRepositoryMockCreateRecord { + if mmCreateRecord.mock.inspectFuncCreateRecord != nil { + mmCreateRecord.mock.t.Fatalf("Inspect function is already set for CacheRepositoryMock.CreateRecord") + } + + mmCreateRecord.mock.inspectFuncCreateRecord = f + + return mmCreateRecord +} + +// Return sets up results that will be returned by CacheRepository.CreateRecord +func (mmCreateRecord *mCacheRepositoryMockCreateRecord) Return(err error) *CacheRepositoryMock { + if mmCreateRecord.mock.funcCreateRecord != nil { + mmCreateRecord.mock.t.Fatalf("CacheRepositoryMock.CreateRecord mock is already set by Set") + } + + if mmCreateRecord.defaultExpectation == nil { + mmCreateRecord.defaultExpectation = &CacheRepositoryMockCreateRecordExpectation{mock: mmCreateRecord.mock} + } + mmCreateRecord.defaultExpectation.results = &CacheRepositoryMockCreateRecordResults{err} + return mmCreateRecord.mock +} + +// Set uses given function f to mock the CacheRepository.CreateRecord method +func (mmCreateRecord *mCacheRepositoryMockCreateRecord) Set(f func(ctx context.Context, user *model.User) (err error)) *CacheRepositoryMock { + if mmCreateRecord.defaultExpectation != nil { + mmCreateRecord.mock.t.Fatalf("Default expectation is already set for the CacheRepository.CreateRecord method") + } + + if len(mmCreateRecord.expectations) > 0 { + mmCreateRecord.mock.t.Fatalf("Some expectations are already set for the CacheRepository.CreateRecord method") + } + + mmCreateRecord.mock.funcCreateRecord = f + return mmCreateRecord.mock +} + +// When sets expectation for the CacheRepository.CreateRecord which will trigger the result defined by the following +// Then helper +func (mmCreateRecord *mCacheRepositoryMockCreateRecord) When(ctx context.Context, user *model.User) *CacheRepositoryMockCreateRecordExpectation { + if mmCreateRecord.mock.funcCreateRecord != nil { + mmCreateRecord.mock.t.Fatalf("CacheRepositoryMock.CreateRecord mock is already set by Set") + } + + expectation := &CacheRepositoryMockCreateRecordExpectation{ + mock: mmCreateRecord.mock, + params: &CacheRepositoryMockCreateRecordParams{ctx, user}, + } + mmCreateRecord.expectations = append(mmCreateRecord.expectations, expectation) + return expectation +} + +// Then sets up CacheRepository.CreateRecord return parameters for the expectation previously defined by the When method +func (e *CacheRepositoryMockCreateRecordExpectation) Then(err error) *CacheRepositoryMock { + e.results = &CacheRepositoryMockCreateRecordResults{err} + return e.mock +} + +// Times sets number of times CacheRepository.CreateRecord should be invoked +func (mmCreateRecord *mCacheRepositoryMockCreateRecord) Times(n uint64) *mCacheRepositoryMockCreateRecord { + if n == 0 { + mmCreateRecord.mock.t.Fatalf("Times of CacheRepositoryMock.CreateRecord mock can not be zero") + } + mm_atomic.StoreUint64(&mmCreateRecord.expectedInvocations, n) + return mmCreateRecord +} + +func (mmCreateRecord *mCacheRepositoryMockCreateRecord) invocationsDone() bool { + if len(mmCreateRecord.expectations) == 0 && mmCreateRecord.defaultExpectation == nil && mmCreateRecord.mock.funcCreateRecord == nil { + return true + } + + totalInvocations := mm_atomic.LoadUint64(&mmCreateRecord.mock.afterCreateRecordCounter) + expectedInvocations := mm_atomic.LoadUint64(&mmCreateRecord.expectedInvocations) + + return totalInvocations > 0 && (expectedInvocations == 0 || expectedInvocations == totalInvocations) +} + +// CreateRecord implements repository.CacheRepository +func (mmCreateRecord *CacheRepositoryMock) CreateRecord(ctx context.Context, user *model.User) (err error) { + mm_atomic.AddUint64(&mmCreateRecord.beforeCreateRecordCounter, 1) + defer mm_atomic.AddUint64(&mmCreateRecord.afterCreateRecordCounter, 1) + + if mmCreateRecord.inspectFuncCreateRecord != nil { + mmCreateRecord.inspectFuncCreateRecord(ctx, user) + } + + mm_params := CacheRepositoryMockCreateRecordParams{ctx, user} + + // Record call args + mmCreateRecord.CreateRecordMock.mutex.Lock() + mmCreateRecord.CreateRecordMock.callArgs = append(mmCreateRecord.CreateRecordMock.callArgs, &mm_params) + mmCreateRecord.CreateRecordMock.mutex.Unlock() + + for _, e := range mmCreateRecord.CreateRecordMock.expectations { + if minimock.Equal(*e.params, mm_params) { + mm_atomic.AddUint64(&e.Counter, 1) + return e.results.err + } + } + + if mmCreateRecord.CreateRecordMock.defaultExpectation != nil { + mm_atomic.AddUint64(&mmCreateRecord.CreateRecordMock.defaultExpectation.Counter, 1) + mm_want := mmCreateRecord.CreateRecordMock.defaultExpectation.params + mm_want_ptrs := mmCreateRecord.CreateRecordMock.defaultExpectation.paramPtrs + + mm_got := CacheRepositoryMockCreateRecordParams{ctx, user} + + if mm_want_ptrs != nil { + + if mm_want_ptrs.ctx != nil && !minimock.Equal(*mm_want_ptrs.ctx, mm_got.ctx) { + mmCreateRecord.t.Errorf("CacheRepositoryMock.CreateRecord got unexpected parameter ctx, want: %#v, got: %#v%s\n", *mm_want_ptrs.ctx, mm_got.ctx, minimock.Diff(*mm_want_ptrs.ctx, mm_got.ctx)) + } + + if mm_want_ptrs.user != nil && !minimock.Equal(*mm_want_ptrs.user, mm_got.user) { + mmCreateRecord.t.Errorf("CacheRepositoryMock.CreateRecord got unexpected parameter user, want: %#v, got: %#v%s\n", *mm_want_ptrs.user, mm_got.user, minimock.Diff(*mm_want_ptrs.user, mm_got.user)) + } + + } else if mm_want != nil && !minimock.Equal(*mm_want, mm_got) { + mmCreateRecord.t.Errorf("CacheRepositoryMock.CreateRecord got unexpected parameters, want: %#v, got: %#v%s\n", *mm_want, mm_got, minimock.Diff(*mm_want, mm_got)) + } + + mm_results := mmCreateRecord.CreateRecordMock.defaultExpectation.results + if mm_results == nil { + mmCreateRecord.t.Fatal("No results are set for the CacheRepositoryMock.CreateRecord") + } + return (*mm_results).err + } + if mmCreateRecord.funcCreateRecord != nil { + return mmCreateRecord.funcCreateRecord(ctx, user) + } + mmCreateRecord.t.Fatalf("Unexpected call to CacheRepositoryMock.CreateRecord. %v %v", ctx, user) + return +} + +// CreateRecordAfterCounter returns a count of finished CacheRepositoryMock.CreateRecord invocations +func (mmCreateRecord *CacheRepositoryMock) CreateRecordAfterCounter() uint64 { + return mm_atomic.LoadUint64(&mmCreateRecord.afterCreateRecordCounter) +} + +// CreateRecordBeforeCounter returns a count of CacheRepositoryMock.CreateRecord invocations +func (mmCreateRecord *CacheRepositoryMock) CreateRecordBeforeCounter() uint64 { + return mm_atomic.LoadUint64(&mmCreateRecord.beforeCreateRecordCounter) +} + +// Calls returns a list of arguments used in each call to CacheRepositoryMock.CreateRecord. +// The list is in the same order as the calls were made (i.e. recent calls have a higher index) +func (mmCreateRecord *mCacheRepositoryMockCreateRecord) Calls() []*CacheRepositoryMockCreateRecordParams { + mmCreateRecord.mutex.RLock() + + argCopy := make([]*CacheRepositoryMockCreateRecordParams, len(mmCreateRecord.callArgs)) + copy(argCopy, mmCreateRecord.callArgs) + + mmCreateRecord.mutex.RUnlock() + + return argCopy +} + +// MinimockCreateRecordDone returns true if the count of the CreateRecord invocations corresponds +// the number of defined expectations +func (m *CacheRepositoryMock) MinimockCreateRecordDone() bool { + if m.CreateRecordMock.optional { + // Optional methods provide '0 or more' call count restriction. + return true + } + + for _, e := range m.CreateRecordMock.expectations { + if mm_atomic.LoadUint64(&e.Counter) < 1 { + return false + } + } + + return m.CreateRecordMock.invocationsDone() +} + +// MinimockCreateRecordInspect logs each unmet expectation +func (m *CacheRepositoryMock) MinimockCreateRecordInspect() { + for _, e := range m.CreateRecordMock.expectations { + if mm_atomic.LoadUint64(&e.Counter) < 1 { + m.t.Errorf("Expected call to CacheRepositoryMock.CreateRecord with params: %#v", *e.params) + } + } + + afterCreateRecordCounter := mm_atomic.LoadUint64(&m.afterCreateRecordCounter) + // if default expectation was set then invocations count should be greater than zero + if m.CreateRecordMock.defaultExpectation != nil && afterCreateRecordCounter < 1 { + if m.CreateRecordMock.defaultExpectation.params == nil { + m.t.Error("Expected call to CacheRepositoryMock.CreateRecord") + } else { + m.t.Errorf("Expected call to CacheRepositoryMock.CreateRecord with params: %#v", *m.CreateRecordMock.defaultExpectation.params) + } + } + // if func was set then invocations count should be greater than zero + if m.funcCreateRecord != nil && afterCreateRecordCounter < 1 { + m.t.Error("Expected call to CacheRepositoryMock.CreateRecord") + } + + if !m.CreateRecordMock.invocationsDone() && afterCreateRecordCounter > 0 { + m.t.Errorf("Expected %d calls to CacheRepositoryMock.CreateRecord but found %d calls", + mm_atomic.LoadUint64(&m.CreateRecordMock.expectedInvocations), afterCreateRecordCounter) + } +} + +type mCacheRepositoryMockGetRecord struct { + optional bool + mock *CacheRepositoryMock + defaultExpectation *CacheRepositoryMockGetRecordExpectation + expectations []*CacheRepositoryMockGetRecordExpectation + + callArgs []*CacheRepositoryMockGetRecordParams + mutex sync.RWMutex + + expectedInvocations uint64 +} + +// CacheRepositoryMockGetRecordExpectation specifies expectation struct of the CacheRepository.GetRecord +type CacheRepositoryMockGetRecordExpectation struct { + mock *CacheRepositoryMock + params *CacheRepositoryMockGetRecordParams + paramPtrs *CacheRepositoryMockGetRecordParamPtrs + results *CacheRepositoryMockGetRecordResults + Counter uint64 +} + +// CacheRepositoryMockGetRecordParams contains parameters of the CacheRepository.GetRecord +type CacheRepositoryMockGetRecordParams struct { + ctx context.Context + id int64 +} + +// CacheRepositoryMockGetRecordParamPtrs contains pointers to parameters of the CacheRepository.GetRecord +type CacheRepositoryMockGetRecordParamPtrs struct { + ctx *context.Context + id *int64 +} + +// CacheRepositoryMockGetRecordResults contains results of the CacheRepository.GetRecord +type CacheRepositoryMockGetRecordResults struct { + up1 *model.User + err error +} + +// Marks this method to be optional. The default behavior of any method with Return() is '1 or more', meaning +// the test will fail minimock's automatic final call check if the mocked method was not called at least once. +// Optional() makes method check to work in '0 or more' mode. +// It is NOT RECOMMENDED to use this option unless you really need it, as default behaviour helps to +// catch the problems when the expected method call is totally skipped during test run. +func (mmGetRecord *mCacheRepositoryMockGetRecord) Optional() *mCacheRepositoryMockGetRecord { + mmGetRecord.optional = true + return mmGetRecord +} + +// Expect sets up expected params for CacheRepository.GetRecord +func (mmGetRecord *mCacheRepositoryMockGetRecord) Expect(ctx context.Context, id int64) *mCacheRepositoryMockGetRecord { + if mmGetRecord.mock.funcGetRecord != nil { + mmGetRecord.mock.t.Fatalf("CacheRepositoryMock.GetRecord mock is already set by Set") + } + + if mmGetRecord.defaultExpectation == nil { + mmGetRecord.defaultExpectation = &CacheRepositoryMockGetRecordExpectation{} + } + + if mmGetRecord.defaultExpectation.paramPtrs != nil { + mmGetRecord.mock.t.Fatalf("CacheRepositoryMock.GetRecord mock is already set by ExpectParams functions") + } + + mmGetRecord.defaultExpectation.params = &CacheRepositoryMockGetRecordParams{ctx, id} + for _, e := range mmGetRecord.expectations { + if minimock.Equal(e.params, mmGetRecord.defaultExpectation.params) { + mmGetRecord.mock.t.Fatalf("Expectation set by When has same params: %#v", *mmGetRecord.defaultExpectation.params) + } + } + + return mmGetRecord +} + +// ExpectCtxParam1 sets up expected param ctx for CacheRepository.GetRecord +func (mmGetRecord *mCacheRepositoryMockGetRecord) ExpectCtxParam1(ctx context.Context) *mCacheRepositoryMockGetRecord { + if mmGetRecord.mock.funcGetRecord != nil { + mmGetRecord.mock.t.Fatalf("CacheRepositoryMock.GetRecord mock is already set by Set") + } + + if mmGetRecord.defaultExpectation == nil { + mmGetRecord.defaultExpectation = &CacheRepositoryMockGetRecordExpectation{} + } + + if mmGetRecord.defaultExpectation.params != nil { + mmGetRecord.mock.t.Fatalf("CacheRepositoryMock.GetRecord mock is already set by Expect") + } + + if mmGetRecord.defaultExpectation.paramPtrs == nil { + mmGetRecord.defaultExpectation.paramPtrs = &CacheRepositoryMockGetRecordParamPtrs{} + } + mmGetRecord.defaultExpectation.paramPtrs.ctx = &ctx + + return mmGetRecord +} + +// ExpectIdParam2 sets up expected param id for CacheRepository.GetRecord +func (mmGetRecord *mCacheRepositoryMockGetRecord) ExpectIdParam2(id int64) *mCacheRepositoryMockGetRecord { + if mmGetRecord.mock.funcGetRecord != nil { + mmGetRecord.mock.t.Fatalf("CacheRepositoryMock.GetRecord mock is already set by Set") + } + + if mmGetRecord.defaultExpectation == nil { + mmGetRecord.defaultExpectation = &CacheRepositoryMockGetRecordExpectation{} + } + + if mmGetRecord.defaultExpectation.params != nil { + mmGetRecord.mock.t.Fatalf("CacheRepositoryMock.GetRecord mock is already set by Expect") + } + + if mmGetRecord.defaultExpectation.paramPtrs == nil { + mmGetRecord.defaultExpectation.paramPtrs = &CacheRepositoryMockGetRecordParamPtrs{} + } + mmGetRecord.defaultExpectation.paramPtrs.id = &id + + return mmGetRecord +} + +// Inspect accepts an inspector function that has same arguments as the CacheRepository.GetRecord +func (mmGetRecord *mCacheRepositoryMockGetRecord) Inspect(f func(ctx context.Context, id int64)) *mCacheRepositoryMockGetRecord { + if mmGetRecord.mock.inspectFuncGetRecord != nil { + mmGetRecord.mock.t.Fatalf("Inspect function is already set for CacheRepositoryMock.GetRecord") + } + + mmGetRecord.mock.inspectFuncGetRecord = f + + return mmGetRecord +} + +// Return sets up results that will be returned by CacheRepository.GetRecord +func (mmGetRecord *mCacheRepositoryMockGetRecord) Return(up1 *model.User, err error) *CacheRepositoryMock { + if mmGetRecord.mock.funcGetRecord != nil { + mmGetRecord.mock.t.Fatalf("CacheRepositoryMock.GetRecord mock is already set by Set") + } + + if mmGetRecord.defaultExpectation == nil { + mmGetRecord.defaultExpectation = &CacheRepositoryMockGetRecordExpectation{mock: mmGetRecord.mock} + } + mmGetRecord.defaultExpectation.results = &CacheRepositoryMockGetRecordResults{up1, err} + return mmGetRecord.mock +} + +// Set uses given function f to mock the CacheRepository.GetRecord method +func (mmGetRecord *mCacheRepositoryMockGetRecord) Set(f func(ctx context.Context, id int64) (up1 *model.User, err error)) *CacheRepositoryMock { + if mmGetRecord.defaultExpectation != nil { + mmGetRecord.mock.t.Fatalf("Default expectation is already set for the CacheRepository.GetRecord method") + } + + if len(mmGetRecord.expectations) > 0 { + mmGetRecord.mock.t.Fatalf("Some expectations are already set for the CacheRepository.GetRecord method") + } + + mmGetRecord.mock.funcGetRecord = f + return mmGetRecord.mock +} + +// When sets expectation for the CacheRepository.GetRecord which will trigger the result defined by the following +// Then helper +func (mmGetRecord *mCacheRepositoryMockGetRecord) When(ctx context.Context, id int64) *CacheRepositoryMockGetRecordExpectation { + if mmGetRecord.mock.funcGetRecord != nil { + mmGetRecord.mock.t.Fatalf("CacheRepositoryMock.GetRecord mock is already set by Set") + } + + expectation := &CacheRepositoryMockGetRecordExpectation{ + mock: mmGetRecord.mock, + params: &CacheRepositoryMockGetRecordParams{ctx, id}, + } + mmGetRecord.expectations = append(mmGetRecord.expectations, expectation) + return expectation +} + +// Then sets up CacheRepository.GetRecord return parameters for the expectation previously defined by the When method +func (e *CacheRepositoryMockGetRecordExpectation) Then(up1 *model.User, err error) *CacheRepositoryMock { + e.results = &CacheRepositoryMockGetRecordResults{up1, err} + return e.mock +} + +// Times sets number of times CacheRepository.GetRecord should be invoked +func (mmGetRecord *mCacheRepositoryMockGetRecord) Times(n uint64) *mCacheRepositoryMockGetRecord { + if n == 0 { + mmGetRecord.mock.t.Fatalf("Times of CacheRepositoryMock.GetRecord mock can not be zero") + } + mm_atomic.StoreUint64(&mmGetRecord.expectedInvocations, n) + return mmGetRecord +} + +func (mmGetRecord *mCacheRepositoryMockGetRecord) invocationsDone() bool { + if len(mmGetRecord.expectations) == 0 && mmGetRecord.defaultExpectation == nil && mmGetRecord.mock.funcGetRecord == nil { + return true + } + + totalInvocations := mm_atomic.LoadUint64(&mmGetRecord.mock.afterGetRecordCounter) + expectedInvocations := mm_atomic.LoadUint64(&mmGetRecord.expectedInvocations) + + return totalInvocations > 0 && (expectedInvocations == 0 || expectedInvocations == totalInvocations) +} + +// GetRecord implements repository.CacheRepository +func (mmGetRecord *CacheRepositoryMock) GetRecord(ctx context.Context, id int64) (up1 *model.User, err error) { + mm_atomic.AddUint64(&mmGetRecord.beforeGetRecordCounter, 1) + defer mm_atomic.AddUint64(&mmGetRecord.afterGetRecordCounter, 1) + + if mmGetRecord.inspectFuncGetRecord != nil { + mmGetRecord.inspectFuncGetRecord(ctx, id) + } + + mm_params := CacheRepositoryMockGetRecordParams{ctx, id} + + // Record call args + mmGetRecord.GetRecordMock.mutex.Lock() + mmGetRecord.GetRecordMock.callArgs = append(mmGetRecord.GetRecordMock.callArgs, &mm_params) + mmGetRecord.GetRecordMock.mutex.Unlock() + + for _, e := range mmGetRecord.GetRecordMock.expectations { + if minimock.Equal(*e.params, mm_params) { + mm_atomic.AddUint64(&e.Counter, 1) + return e.results.up1, e.results.err + } + } + + if mmGetRecord.GetRecordMock.defaultExpectation != nil { + mm_atomic.AddUint64(&mmGetRecord.GetRecordMock.defaultExpectation.Counter, 1) + mm_want := mmGetRecord.GetRecordMock.defaultExpectation.params + mm_want_ptrs := mmGetRecord.GetRecordMock.defaultExpectation.paramPtrs + + mm_got := CacheRepositoryMockGetRecordParams{ctx, id} + + if mm_want_ptrs != nil { + + if mm_want_ptrs.ctx != nil && !minimock.Equal(*mm_want_ptrs.ctx, mm_got.ctx) { + mmGetRecord.t.Errorf("CacheRepositoryMock.GetRecord got unexpected parameter ctx, want: %#v, got: %#v%s\n", *mm_want_ptrs.ctx, mm_got.ctx, minimock.Diff(*mm_want_ptrs.ctx, mm_got.ctx)) + } + + if mm_want_ptrs.id != nil && !minimock.Equal(*mm_want_ptrs.id, mm_got.id) { + mmGetRecord.t.Errorf("CacheRepositoryMock.GetRecord got unexpected parameter id, want: %#v, got: %#v%s\n", *mm_want_ptrs.id, mm_got.id, minimock.Diff(*mm_want_ptrs.id, mm_got.id)) + } + + } else if mm_want != nil && !minimock.Equal(*mm_want, mm_got) { + mmGetRecord.t.Errorf("CacheRepositoryMock.GetRecord got unexpected parameters, want: %#v, got: %#v%s\n", *mm_want, mm_got, minimock.Diff(*mm_want, mm_got)) + } + + mm_results := mmGetRecord.GetRecordMock.defaultExpectation.results + if mm_results == nil { + mmGetRecord.t.Fatal("No results are set for the CacheRepositoryMock.GetRecord") + } + return (*mm_results).up1, (*mm_results).err + } + if mmGetRecord.funcGetRecord != nil { + return mmGetRecord.funcGetRecord(ctx, id) + } + mmGetRecord.t.Fatalf("Unexpected call to CacheRepositoryMock.GetRecord. %v %v", ctx, id) + return +} + +// GetRecordAfterCounter returns a count of finished CacheRepositoryMock.GetRecord invocations +func (mmGetRecord *CacheRepositoryMock) GetRecordAfterCounter() uint64 { + return mm_atomic.LoadUint64(&mmGetRecord.afterGetRecordCounter) +} + +// GetRecordBeforeCounter returns a count of CacheRepositoryMock.GetRecord invocations +func (mmGetRecord *CacheRepositoryMock) GetRecordBeforeCounter() uint64 { + return mm_atomic.LoadUint64(&mmGetRecord.beforeGetRecordCounter) +} + +// Calls returns a list of arguments used in each call to CacheRepositoryMock.GetRecord. +// The list is in the same order as the calls were made (i.e. recent calls have a higher index) +func (mmGetRecord *mCacheRepositoryMockGetRecord) Calls() []*CacheRepositoryMockGetRecordParams { + mmGetRecord.mutex.RLock() + + argCopy := make([]*CacheRepositoryMockGetRecordParams, len(mmGetRecord.callArgs)) + copy(argCopy, mmGetRecord.callArgs) + + mmGetRecord.mutex.RUnlock() + + return argCopy +} + +// MinimockGetRecordDone returns true if the count of the GetRecord invocations corresponds +// the number of defined expectations +func (m *CacheRepositoryMock) MinimockGetRecordDone() bool { + if m.GetRecordMock.optional { + // Optional methods provide '0 or more' call count restriction. + return true + } + + for _, e := range m.GetRecordMock.expectations { + if mm_atomic.LoadUint64(&e.Counter) < 1 { + return false + } + } + + return m.GetRecordMock.invocationsDone() +} + +// MinimockGetRecordInspect logs each unmet expectation +func (m *CacheRepositoryMock) MinimockGetRecordInspect() { + for _, e := range m.GetRecordMock.expectations { + if mm_atomic.LoadUint64(&e.Counter) < 1 { + m.t.Errorf("Expected call to CacheRepositoryMock.GetRecord with params: %#v", *e.params) + } + } + + afterGetRecordCounter := mm_atomic.LoadUint64(&m.afterGetRecordCounter) + // if default expectation was set then invocations count should be greater than zero + if m.GetRecordMock.defaultExpectation != nil && afterGetRecordCounter < 1 { + if m.GetRecordMock.defaultExpectation.params == nil { + m.t.Error("Expected call to CacheRepositoryMock.GetRecord") + } else { + m.t.Errorf("Expected call to CacheRepositoryMock.GetRecord with params: %#v", *m.GetRecordMock.defaultExpectation.params) + } + } + // if func was set then invocations count should be greater than zero + if m.funcGetRecord != nil && afterGetRecordCounter < 1 { + m.t.Error("Expected call to CacheRepositoryMock.GetRecord") + } + + if !m.GetRecordMock.invocationsDone() && afterGetRecordCounter > 0 { + m.t.Errorf("Expected %d calls to CacheRepositoryMock.GetRecord but found %d calls", + mm_atomic.LoadUint64(&m.GetRecordMock.expectedInvocations), afterGetRecordCounter) + } +} + +// MinimockFinish checks that all mocked methods have been called the expected number of times +func (m *CacheRepositoryMock) MinimockFinish() { + m.finishOnce.Do(func() { + if !m.minimockDone() { + m.MinimockCreateRecordInspect() + + m.MinimockGetRecordInspect() + } + }) +} + +// MinimockWait waits for all mocked methods to be called the expected number of times +func (m *CacheRepositoryMock) MinimockWait(timeout mm_time.Duration) { + timeoutCh := mm_time.After(timeout) + for { + if m.minimockDone() { + return + } + select { + case <-timeoutCh: + m.MinimockFinish() + return + case <-mm_time.After(10 * mm_time.Millisecond): + } + } +} + +func (m *CacheRepositoryMock) minimockDone() bool { + done := true + return done && + m.MinimockCreateRecordDone() && + m.MinimockGetRecordDone() +} diff --git a/internal/repository/repository.go b/internal/repository/repository.go index ab829b2..aaaf2a0 100644 --- a/internal/repository/repository.go +++ b/internal/repository/repository.go @@ -29,3 +29,9 @@ type AccessRepository interface { type LogRepository interface { Log(ctx context.Context, log *model.Log) error } + +// CacheRepository is the interface for cache communication. +type CacheRepository interface { + CreateRecord(ctx context.Context, user *model.User) error + GetRecord(ctx context.Context, id int64) (*model.User, error) +} diff --git a/internal/service/auth/login.go b/internal/service/auth/login.go index 3b4d42b..e23ad35 100644 --- a/internal/service/auth/login.go +++ b/internal/service/auth/login.go @@ -13,7 +13,7 @@ func (s *serv) Login(ctx context.Context, creds *model.UserCreds) (string, error // Get role and hashed password by username from storage authInfo, err := s.userRepository.GetAuthInfo(ctx, creds.Username) if err != nil { - return "", errors.New("user not found") + return "", model.ErrorUserNotFound } err = bcrypt.CompareHashAndPassword([]byte(authInfo.Password), []byte(creds.Password)) diff --git a/internal/service/user/get.go b/internal/service/user/get.go index 60762e4..09897dc 100644 --- a/internal/service/user/get.go +++ b/internal/service/user/get.go @@ -5,12 +5,26 @@ import ( "errors" "fmt" + "go.uber.org/zap" + "github.com/polshe-v/microservices_auth/internal/model" + "github.com/polshe-v/microservices_common/pkg/logger" ) func (s *serv) Get(ctx context.Context, id int64) (*model.User, error) { var user *model.User - err := s.txManager.ReadCommitted(ctx, func(ctx context.Context) error { + + // Check cache first + user, err := s.cacheRepository.GetRecord(ctx, id) + if err == nil { + return user, nil + } + if err != model.ErrorUserNotFound { + logger.Error("failed to read from cache: ", zap.Error(err)) + } + + // If record not found in cache, then get it from DB and put into cache + err = s.txManager.ReadCommitted(ctx, func(ctx context.Context) error { var errTx error user, errTx = s.userRepository.Get(ctx, id) if errTx != nil { @@ -30,5 +44,11 @@ func (s *serv) Get(ctx context.Context, id int64) (*model.User, error) { if err != nil { return nil, errors.New("failed to read user info") } + + err = s.cacheRepository.CreateRecord(ctx, user) + if err != nil { + logger.Error("failed to write to cache: ", zap.Error(err)) + } + return user, nil } diff --git a/internal/service/user/service.go b/internal/service/user/service.go index 4ac8dcd..7c5c842 100644 --- a/internal/service/user/service.go +++ b/internal/service/user/service.go @@ -7,16 +7,18 @@ import ( ) type serv struct { - userRepository repository.UserRepository - logRepository repository.LogRepository - txManager db.TxManager + userRepository repository.UserRepository + cacheRepository repository.CacheRepository + logRepository repository.LogRepository + txManager db.TxManager } // NewService creates new object of service layer. -func NewService(userRepository repository.UserRepository, logRepository repository.LogRepository, txManager db.TxManager) service.UserService { +func NewService(userRepository repository.UserRepository, cacheRepository repository.CacheRepository, logRepository repository.LogRepository, txManager db.TxManager) service.UserService { return &serv{ - userRepository: userRepository, - logRepository: logRepository, - txManager: txManager, + userRepository: userRepository, + cacheRepository: cacheRepository, + logRepository: logRepository, + txManager: txManager, } }