Skip to content
Snippets Groups Projects
Commit c38b1219 authored by Lyuben Penkovski's avatar Lyuben Penkovski
Browse files

Add cache service unit tests

parent 92a43217
No related branches found
No related tags found
1 merge request!4Make the namespace and scope fields optional when setting and getting values from cache
Pipeline #50589 passed
Showing
with 414 additions and 11 deletions
[![pipeline status](https://code.vereign.com/gaiax/tsa/cache/badges/main/pipeline.svg)](https://code.vereign.com/gaiax/tsa/cache/-/commits/main)
[![coverage report](https://code.vereign.com/gaiax/tsa/cache/badges/main/coverage.svg)](https://code.vereign.com/gaiax/tsa/cache/-/commits/main)
# Cache service
Cache service exposes HTTP interface for working with Redis.
### Basic Architecture
```mermaid
flowchart LR
A[Client] -- request --> B[Cache Service] --> C[(Redis)]
```mermaid
flowchart LR
A[Client] -- request --> B[HTTP API]
subgraph cache
B --> C[(Redis)]
end
```
### API Documentation
The API Documentation is accessible at `/swagger-ui` path in OAS 3.0 format.
Example: `http://localhost:8080/swagger-ui`
The API Documentation is accessible at `/swagger-ui` path in OAS 3.0 format. If you
use the docker-compose environment, it's exposed at `http://localhost:8083/swagger-ui`
### Dependencies
There must be a running instance of [Redis](https://redis.io/) visible to the service.
The address, username and password of the Redis in-memory store instance must be provided as environment variables.
The address, username and password of Redis must be provided as environment variables.
Example:
```
......@@ -28,11 +34,12 @@ REDIS_PASS="pass"
### Development
This service uses [Goa framework](https://goa.design/) v3 as a backbone. [This](https://goa.design/learn/getting-started/) is a good starting point for learning to use the framework.
This service uses [Goa framework](https://goa.design/) v3 as a backbone.
[This](https://goa.design/learn/getting-started/) is a good starting point for learning to use the framework.
### Dependencies and Vendor
The project uses Go modules for managing dependencies and we commit the `vendor` directory.
The project uses Go modules for managing dependencies, and we commit the `vendor` directory.
When you add/change dependencies, be sure to clean and update the `vendor` directory before
submitting your Merge Request for review.
```shell
......
......@@ -6,6 +6,7 @@ require (
code.vereign.com/gaiax/tsa/golib v0.0.0-20220321093827-5fdf8f34aad9
github.com/go-redis/redis/v8 v8.11.5
github.com/kelseyhightower/envconfig v1.4.0
github.com/stretchr/testify v1.7.0
go.uber.org/zap v1.21.0
goa.design/goa/v3 v3.7.0
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
......@@ -13,6 +14,7 @@ require (
require (
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dimfeld/httppath v0.0.0-20170720192232-ee938bf73598 // indirect
github.com/dimfeld/httptreemux/v5 v5.4.0 // indirect
......@@ -21,6 +23,7 @@ require (
github.com/gorilla/websocket v1.5.0 // indirect
github.com/jtolds/gls v4.20.0+incompatible // indirect
github.com/manveru/faker v0.0.0-20171103152722-9fbc68a78c4d // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/sergi/go-diff v1.2.0 // indirect
github.com/smartystreets/assertions v1.2.1 // indirect
github.com/zach-klippenstein/goregen v0.0.0-20160303162051-795b5e3961ea // indirect
......@@ -31,4 +34,5 @@ require (
golang.org/x/tools v0.1.10 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
)
// Code generated by counterfeiter. DO NOT EDIT.
package cachefakes
import (
"context"
"sync"
"time"
"code.vereign.com/gaiax/tsa/cache/internal/service/cache"
)
type FakeCache struct {
GetStub func(context.Context, string) ([]byte, error)
getMutex sync.RWMutex
getArgsForCall []struct {
arg1 context.Context
arg2 string
}
getReturns struct {
result1 []byte
result2 error
}
getReturnsOnCall map[int]struct {
result1 []byte
result2 error
}
SetStub func(context.Context, string, []byte, time.Duration) error
setMutex sync.RWMutex
setArgsForCall []struct {
arg1 context.Context
arg2 string
arg3 []byte
arg4 time.Duration
}
setReturns struct {
result1 error
}
setReturnsOnCall map[int]struct {
result1 error
}
invocations map[string][][]interface{}
invocationsMutex sync.RWMutex
}
func (fake *FakeCache) Get(arg1 context.Context, arg2 string) ([]byte, error) {
fake.getMutex.Lock()
ret, specificReturn := fake.getReturnsOnCall[len(fake.getArgsForCall)]
fake.getArgsForCall = append(fake.getArgsForCall, struct {
arg1 context.Context
arg2 string
}{arg1, arg2})
stub := fake.GetStub
fakeReturns := fake.getReturns
fake.recordInvocation("Get", []interface{}{arg1, arg2})
fake.getMutex.Unlock()
if stub != nil {
return stub(arg1, arg2)
}
if specificReturn {
return ret.result1, ret.result2
}
return fakeReturns.result1, fakeReturns.result2
}
func (fake *FakeCache) GetCallCount() int {
fake.getMutex.RLock()
defer fake.getMutex.RUnlock()
return len(fake.getArgsForCall)
}
func (fake *FakeCache) GetCalls(stub func(context.Context, string) ([]byte, error)) {
fake.getMutex.Lock()
defer fake.getMutex.Unlock()
fake.GetStub = stub
}
func (fake *FakeCache) GetArgsForCall(i int) (context.Context, string) {
fake.getMutex.RLock()
defer fake.getMutex.RUnlock()
argsForCall := fake.getArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2
}
func (fake *FakeCache) GetReturns(result1 []byte, result2 error) {
fake.getMutex.Lock()
defer fake.getMutex.Unlock()
fake.GetStub = nil
fake.getReturns = struct {
result1 []byte
result2 error
}{result1, result2}
}
func (fake *FakeCache) GetReturnsOnCall(i int, result1 []byte, result2 error) {
fake.getMutex.Lock()
defer fake.getMutex.Unlock()
fake.GetStub = nil
if fake.getReturnsOnCall == nil {
fake.getReturnsOnCall = make(map[int]struct {
result1 []byte
result2 error
})
}
fake.getReturnsOnCall[i] = struct {
result1 []byte
result2 error
}{result1, result2}
}
func (fake *FakeCache) Set(arg1 context.Context, arg2 string, arg3 []byte, arg4 time.Duration) error {
var arg3Copy []byte
if arg3 != nil {
arg3Copy = make([]byte, len(arg3))
copy(arg3Copy, arg3)
}
fake.setMutex.Lock()
ret, specificReturn := fake.setReturnsOnCall[len(fake.setArgsForCall)]
fake.setArgsForCall = append(fake.setArgsForCall, struct {
arg1 context.Context
arg2 string
arg3 []byte
arg4 time.Duration
}{arg1, arg2, arg3Copy, arg4})
stub := fake.SetStub
fakeReturns := fake.setReturns
fake.recordInvocation("Set", []interface{}{arg1, arg2, arg3Copy, arg4})
fake.setMutex.Unlock()
if stub != nil {
return stub(arg1, arg2, arg3, arg4)
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *FakeCache) SetCallCount() int {
fake.setMutex.RLock()
defer fake.setMutex.RUnlock()
return len(fake.setArgsForCall)
}
func (fake *FakeCache) SetCalls(stub func(context.Context, string, []byte, time.Duration) error) {
fake.setMutex.Lock()
defer fake.setMutex.Unlock()
fake.SetStub = stub
}
func (fake *FakeCache) SetArgsForCall(i int) (context.Context, string, []byte, time.Duration) {
fake.setMutex.RLock()
defer fake.setMutex.RUnlock()
argsForCall := fake.setArgsForCall[i]
return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4
}
func (fake *FakeCache) SetReturns(result1 error) {
fake.setMutex.Lock()
defer fake.setMutex.Unlock()
fake.SetStub = nil
fake.setReturns = struct {
result1 error
}{result1}
}
func (fake *FakeCache) SetReturnsOnCall(i int, result1 error) {
fake.setMutex.Lock()
defer fake.setMutex.Unlock()
fake.SetStub = nil
if fake.setReturnsOnCall == nil {
fake.setReturnsOnCall = make(map[int]struct {
result1 error
})
}
fake.setReturnsOnCall[i] = struct {
result1 error
}{result1}
}
func (fake *FakeCache) Invocations() map[string][][]interface{} {
fake.invocationsMutex.RLock()
defer fake.invocationsMutex.RUnlock()
fake.getMutex.RLock()
defer fake.getMutex.RUnlock()
fake.setMutex.RLock()
defer fake.setMutex.RUnlock()
copiedInvocations := map[string][][]interface{}{}
for key, value := range fake.invocations {
copiedInvocations[key] = value
}
return copiedInvocations
}
func (fake *FakeCache) recordInvocation(key string, args []interface{}) {
fake.invocationsMutex.Lock()
defer fake.invocationsMutex.Unlock()
if fake.invocations == nil {
fake.invocations = map[string][][]interface{}{}
}
if fake.invocations[key] == nil {
fake.invocations[key] = [][]interface{}{}
}
fake.invocations[key] = append(fake.invocations[key], args)
}
var _ cache.Cache = new(FakeCache)
......@@ -11,6 +11,8 @@ import (
"code.vereign.com/gaiax/tsa/golib/errors"
)
//go:generate counterfeiter . Cache
type Cache interface {
Get(ctx context.Context, key string) ([]byte, error)
Set(ctx context.Context, key string, value []byte, ttl time.Duration) error
......@@ -33,7 +35,7 @@ func (s *Service) Get(ctx context.Context, req *cache.CacheGetRequest) (interfac
if req.Key == "" {
logger.Error("bad request: missing key")
return nil, errors.New(errors.BadRequest, "bad request")
return nil, errors.New(errors.BadRequest, "missing key")
}
var namespace, scope string
......@@ -69,7 +71,7 @@ func (s *Service) Set(ctx context.Context, req *cache.CacheSetRequest) error {
if req.Key == "" {
logger.Error("bad request: missing key")
return errors.New(errors.BadRequest, "bad request")
return errors.New(errors.BadRequest, "missing key")
}
var namespace, scope string
......@@ -88,7 +90,7 @@ func (s *Service) Set(ctx context.Context, req *cache.CacheSetRequest) error {
value, err := json.Marshal(req.Data)
if err != nil {
logger.Error("error encode payload to json", zap.Error(err))
return errors.New("error encode payload to json", err)
return errors.New(errors.BadRequest, "cannot encode payload to json", err)
}
if err := s.cache.Set(ctx, key, value, 0); err != nil {
......
package cache_test
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
"go.uber.org/zap"
goacache "code.vereign.com/gaiax/tsa/cache/gen/cache"
"code.vereign.com/gaiax/tsa/cache/internal/service/cache"
"code.vereign.com/gaiax/tsa/cache/internal/service/cache/cachefakes"
"code.vereign.com/gaiax/tsa/golib/errors"
"code.vereign.com/gaiax/tsa/golib/ptr"
)
func TestNew(t *testing.T) {
svc := cache.New(nil, zap.NewNop())
assert.Implements(t, (*goacache.Service)(nil), svc)
}
func TestService_Get(t *testing.T) {
tests := []struct {
name string
cache *cachefakes.FakeCache
req *goacache.CacheGetRequest
res interface{}
errkind errors.Kind
errtext string
}{
{
name: "missing cache key",
req: &goacache.CacheGetRequest{},
errkind: errors.BadRequest,
errtext: "missing key",
},
{
name: "error getting value from cache",
req: &goacache.CacheGetRequest{
Key: "key",
Namespace: ptr.String("namespace"),
Scope: ptr.String("scope"),
},
cache: &cachefakes.FakeCache{
GetStub: func(ctx context.Context, key string) ([]byte, error) {
return nil, errors.New("some error")
},
},
errkind: errors.Unknown,
errtext: "some error",
},
{
name: "key not found in cache",
req: &goacache.CacheGetRequest{
Key: "key",
Namespace: ptr.String("namespace"),
Scope: ptr.String("scope"),
},
cache: &cachefakes.FakeCache{
GetStub: func(ctx context.Context, key string) ([]byte, error) {
return nil, errors.New(errors.NotFound)
},
},
errkind: errors.NotFound,
errtext: "key not found in cache",
},
{
name: "value returned from cache is not json",
req: &goacache.CacheGetRequest{
Key: "key",
Namespace: ptr.String("namespace"),
Scope: ptr.String("scope"),
},
cache: &cachefakes.FakeCache{
GetStub: func(ctx context.Context, key string) ([]byte, error) {
return []byte("boom"), nil
},
},
errkind: errors.Unknown,
errtext: "cannot decode json value from cache",
},
{
name: "json value is successfully returned from cache",
req: &goacache.CacheGetRequest{
Key: "key",
Namespace: ptr.String("namespace"),
Scope: ptr.String("scope"),
},
cache: &cachefakes.FakeCache{
GetStub: func(ctx context.Context, key string) ([]byte, error) {
return []byte(`{"test":"value"}`), nil
},
},
res: map[string]interface{}{"test": "value"},
errtext: "",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
svc := cache.New(test.cache, zap.NewNop())
res, err := svc.Get(context.Background(), test.req)
if err == nil {
assert.Empty(t, test.errtext)
assert.Equal(t, test.res, res)
} else {
assert.Nil(t, res)
assert.Error(t, err)
e, ok := err.(*errors.Error)
assert.True(t, ok)
assert.Equal(t, test.errkind, e.Kind)
assert.Contains(t, e.Error(), test.errtext)
}
})
}
}
func TestService_Set(t *testing.T) {
tests := []struct {
name string
cache *cachefakes.FakeCache
req *goacache.CacheSetRequest
res interface{}
errkind errors.Kind
errtext string
}{
{
name: "missing cache key",
req: &goacache.CacheSetRequest{},
errkind: errors.BadRequest,
errtext: "missing key",
},
{
name: "error setting value in cache",
req: &goacache.CacheSetRequest{
Key: "key",
Namespace: ptr.String("namespace"),
Scope: ptr.String("scope"),
Data: map[string]interface{}{"test": "value"},
},
cache: &cachefakes.FakeCache{
SetStub: func(ctx context.Context, key string, value []byte, ttl time.Duration) error {
return errors.New(errors.Timeout, "some error")
},
},
errkind: errors.Timeout,
errtext: "some error",
},
{
name: "successfully set value in cache",
req: &goacache.CacheSetRequest{
Key: "key",
Namespace: ptr.String("namespace"),
Scope: ptr.String("scope"),
Data: map[string]interface{}{"test": "value"},
},
cache: &cachefakes.FakeCache{
SetStub: func(ctx context.Context, key string, value []byte, ttl time.Duration) error {
return nil
},
},
errtext: "",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
svc := cache.New(test.cache, zap.NewNop())
err := svc.Set(context.Background(), test.req)
if err == nil {
assert.Empty(t, test.errtext)
} else {
assert.Error(t, err)
e, ok := err.(*errors.Error)
assert.True(t, ok)
assert.Equal(t, test.errkind, e.Kind)
assert.Contains(t, e.Error(), test.errtext)
}
})
}
}
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment