From 7236bbdd7959c541b47793ad26f9bbd69ba1b97a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20=C3=9Cmit=20=C3=96zden?= Date: Thu, 2 Nov 2023 21:28:52 +0300 Subject: [PATCH] test: Implement unit tests for core/app using mock adapters * test: Add cacher and redirection_repo mocks * refactor: Change redirection key data type to string instead of url * fix: Set cache after cache miss * refactor: Change byte array data type which used in cacher port to string for convenience --- go.mod | 9 ++++ go.sum | 12 +++++ internal/core/app/app.go | 25 ++++++--- internal/core/app/app_test.go | 86 ++++++++++++++++++++++++++++++ internal/core/ports/app_runner.go | 2 +- internal/core/ports/cacher.go | 4 +- internal/mocks/cacher.go | 64 ++++++++++++++++++++++ internal/mocks/redirection_repo.go | 28 ++++++++++ 8 files changed, 221 insertions(+), 9 deletions(-) create mode 100644 go.sum create mode 100644 internal/core/app/app_test.go create mode 100644 internal/mocks/cacher.go create mode 100644 internal/mocks/redirection_repo.go diff --git a/go.mod b/go.mod index aa8937e..bb4cde4 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,12 @@ module github.com/mehmetumit/dexus go 1.21.3 + +require ( + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/go-chi/chi/v5 v5.0.10 // indirect + github.com/go-chi/cors v1.2.1 // indirect + github.com/google/uuid v1.4.0 // indirect + github.com/redis/go-redis/v9 v9.2.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..c7d4e57 --- /dev/null +++ b/go.sum @@ -0,0 +1,12 @@ +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk= +github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= +github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= +github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= +github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/redis/go-redis/v9 v9.2.1 h1:WlYJg71ODF0dVspZZCpYmoF1+U1Jjk9Rwd7pq6QmlCg= +github.com/redis/go-redis/v9 v9.2.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= diff --git a/internal/core/app/app.go b/internal/core/app/app.go index 5245ef5..9e9c868 100644 --- a/internal/core/app/app.go +++ b/internal/core/app/app.go @@ -3,10 +3,15 @@ package app import ( "context" "net/url" + "time" "github.com/mehmetumit/dexus/internal/core/ports" ) +var ( + defaultCacheTTL = 10 * time.Second +) + type AppConfig struct { Logger ports.Logger RedirectionRepo ports.RedirectionRepo @@ -22,9 +27,9 @@ func NewApp(cfg AppConfig) *App { } } -func (a *App) FindRedirect(ctx context.Context, u *url.URL) (*url.URL, error) { +func (a *App) FindRedirect(ctx context.Context, p string) (*url.URL, error) { var dataTo string - key, err := a.Cacher.GenKey(ctx, u.String()) + key, err := a.Cacher.GenKey(ctx, p) if err != nil { a.Logger.Error("internal key generation error:", err) return nil, err @@ -35,16 +40,24 @@ func (a *App) FindRedirect(ctx context.Context, u *url.URL) (*url.URL, error) { a.Logger.Error("internal cache error:", err) return nil, err } - a.Logger.Debug("Cache miss:", string(cachedTo)) - redirectionTo, err := a.RedirectionRepo.Get(ctx, u.String()) + a.Logger.Debug("Cache miss:", p) + redirectionTo, err := a.RedirectionRepo.Get(ctx, p) if err != nil { - a.Logger.Error("internal redirection repo error:", err) + if err == ports.ErrRedirectionNotFound { + a.Logger.Error("redirection not found on repo:", err) + } else { + a.Logger.Error("internal cache error:", err) + } return nil, err } + //Set cache after cache miss + if err := a.Cacher.Set(ctx, key, redirectionTo, defaultCacheTTL); err != nil { + a.Logger.Error("cache set error:", err) + } dataTo = redirectionTo } else { - dataTo = string(cachedTo) + dataTo = cachedTo a.Logger.Debug("Cache hit", dataTo) } to, err := url.Parse(dataTo) diff --git a/internal/core/app/app_test.go b/internal/core/app/app_test.go new file mode 100644 index 0000000..5fffa6e --- /dev/null +++ b/internal/core/app/app_test.go @@ -0,0 +1,86 @@ +package app + +import ( + "context" + "testing" + + "github.com/mehmetumit/dexus/internal/core/ports" + "github.com/mehmetumit/dexus/internal/mocks" +) + +func newTestApp(t testing.TB) *App { + t.Helper() + mockLogger := mocks.NewMockLogger() + mockRedirectionRepo := mocks.NewMockRedirectionRepo() + mockCacher := mocks.NewMockCacher() + mockLogger.SetDebugLevel(true) + return NewApp( + AppConfig{ + Logger: mockLogger, + RedirectionRepo: mockRedirectionRepo, + Cacher: mockCacher, + }, + ) +} + +func TestApp_FindRedirect(t *testing.T) { + redirectionMap := mocks.MockRedirectionMap{ + "test1": "https://test1.com", + "test2": "https://test2.com", + "test3": "https://test3.com", + } + notFoundKeys := []string{"not-found", "not/found", ""} + t.Run("Find Redirect From Repo", func(t *testing.T) { + app := newTestApp(t) + ctx := context.Background() + app.RedirectionRepo.(*mocks.MockRedirectionRepo).SetMockRedirectionMap(redirectionMap) + for k, v := range redirectionMap { + u, err := app.FindRedirect(ctx, k) + if err != nil { + t.Errorf("Expected err nil, got %v", err) + } + + if u.String() != v { + t.Errorf("Expected redirection %v, got %v", v, u.String()) + + } + } + }) + t.Run("Find Not Found Redirect From Repo", func(t *testing.T) { + app := newTestApp(t) + app.RedirectionRepo.(*mocks.MockRedirectionRepo).SetMockRedirectionMap(redirectionMap) + for _, v := range notFoundKeys { + _, err := app.FindRedirect(context.Background(), v) + if err != ports.ErrRedirectionNotFound { + t.Errorf("Expected err %v, got %v", ports.ErrRedirectionNotFound, err) + } + } + }) + t.Run("Find Redirect From Cache", func(t *testing.T) { + app := newTestApp(t) + app.RedirectionRepo.(*mocks.MockRedirectionRepo).SetMockRedirectionMap(redirectionMap) + ctx := context.Background() + for k := range redirectionMap { + app.FindRedirect(ctx, k) + } + for k, v := range redirectionMap { + keyHash, err := app.Cacher.GenKey(ctx, k) + if err != nil { + t.Errorf("Expected err nil, got %v", err) + } + gotVal, err := app.Cacher.Get(ctx, keyHash) + if err != nil { + t.Errorf("Expected err nil, got %v", err) + } + if gotVal != v { + t.Errorf("Expected cache val %v, got %v", v, gotVal) + } + _, err = app.FindRedirect(ctx, k) + if err != nil { + t.Errorf("Expected err nil, got %v", err) + } + } + + }) + +} diff --git a/internal/core/ports/app_runner.go b/internal/core/ports/app_runner.go index e5b36dd..0fcca07 100644 --- a/internal/core/ports/app_runner.go +++ b/internal/core/ports/app_runner.go @@ -6,5 +6,5 @@ import ( ) type AppRunner interface { - FindRedirect(ctx context.Context, u *url.URL) (*url.URL, error) + FindRedirect(ctx context.Context, p string) (*url.URL, error) } diff --git a/internal/core/ports/cacher.go b/internal/core/ports/cacher.go index 0638503..b589bf9 100644 --- a/internal/core/ports/cacher.go +++ b/internal/core/ports/cacher.go @@ -12,8 +12,8 @@ var ( type Cacher interface { GenKey(ctx context.Context, s string) (string, error) - Get(ctx context.Context, key string) ([]byte, error) - Set(ctx context.Context, key string, val []byte, ttl time.Duration) error + Get(ctx context.Context, key string) (string, error) + Set(ctx context.Context, key string, val string, ttl time.Duration) error Delete(ctx context.Context, key string) error Flush(ctx context.Context) error } diff --git a/internal/mocks/cacher.go b/internal/mocks/cacher.go new file mode 100644 index 0000000..82acc81 --- /dev/null +++ b/internal/mocks/cacher.go @@ -0,0 +1,64 @@ +package mocks + +import ( + "context" + "sync" + "time" + + "github.com/google/uuid" + "github.com/mehmetumit/dexus/internal/core/ports" +) + +type MockCacheMap map[string]string +type MockCacher struct { + sync.RWMutex //Useful for concurrent nonblocking reads + cacheMap MockCacheMap +} + +func NewMockCacher() *MockCacher { + return &MockCacher{ + cacheMap: make(MockCacheMap), + } +} + +func (mc *MockCacher) expireAfter(key string, ttl time.Duration) { + time.AfterFunc(ttl, func() { + mc.Lock() + defer mc.Unlock() + delete(mc.cacheMap, key) + }) +} +func (mc *MockCacher) GenKey(ctx context.Context, s string) (string, error) { + hashKey := uuid.NewSHA1(uuid.NameSpaceOID, []byte(s)).String() + return hashKey, nil + +} +func (mc *MockCacher) Get(ctx context.Context, key string) (string, error) { + mc.RLock() + defer mc.RUnlock() + val, ok := mc.cacheMap[key] + if !ok { + return "", ports.ErrKeyNotFound + } + return val, nil + +} +func (mc *MockCacher) Set(ctx context.Context, key string, val string, ttl time.Duration) error { + mc.Lock() + defer mc.Unlock() + mc.cacheMap[key] = val + mc.expireAfter(key, ttl) + return nil +} +func (mc *MockCacher) Delete(ctx context.Context, key string) error { + mc.Lock() + defer mc.Unlock() + delete(mc.cacheMap, key) + return nil +} +func (mc *MockCacher) Flush(ctx context.Context) error { + mc.Lock() + defer mc.Unlock() + clear(mc.cacheMap) + return nil +} diff --git a/internal/mocks/redirection_repo.go b/internal/mocks/redirection_repo.go new file mode 100644 index 0000000..de91eeb --- /dev/null +++ b/internal/mocks/redirection_repo.go @@ -0,0 +1,28 @@ +package mocks + +import ( + "context" + + "github.com/mehmetumit/dexus/internal/core/ports" +) + +type MockRedirectionRepo struct { + redirectionMap MockRedirectionMap +} + +type MockRedirectionMap map[string]string + +func NewMockRedirectionRepo(p ...MockRedirectionMap) *MockRedirectionRepo { + return &MockRedirectionRepo{} + +} +func (mr *MockRedirectionRepo) Get(ctx context.Context, from string) (string, error) { + to, ok := mr.redirectionMap[from] + if !ok { + return "", ports.ErrRedirectionNotFound + } + return to, nil +} +func (mr *MockRedirectionRepo) SetMockRedirectionMap(m MockRedirectionMap) { + mr.redirectionMap = m +}