github.com/willyham/dosa@v2.3.1-0.20171024181418-1e446d37ee71+incompatible/connectors/redis/redis_test.go (about)

     1  // Copyright (c) 2017 Uber Technologies, Inc.
     2  //
     3  // Permission is hereby granted, free of charge, to any person obtaining a copy
     4  // of this software and associated documentation files (the "Software"), to deal
     5  // in the Software without restriction, including without limitation the rights
     6  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     7  // copies of the Software, and to permit persons to whom the Software is
     8  // furnished to do so, subject to the following conditions:
     9  //
    10  // The above copyright notice and this permission notice shall be included in
    11  // all copies or substantial portions of the Software.
    12  //
    13  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    14  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    15  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    16  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    17  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    18  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    19  // THE SOFTWARE.
    20  
    21  package redis_test
    22  
    23  import (
    24  	"math/rand"
    25  	"testing"
    26  	"time"
    27  
    28  	"github.com/golang/mock/gomock"
    29  	"github.com/stretchr/testify/assert"
    30  	"github.com/uber-go/dosa"
    31  	"github.com/uber-go/dosa/connectors/redis"
    32  	"github.com/uber-go/dosa/mocks"
    33  	"github.com/uber-go/dosa/testentity"
    34  	"golang.org/x/net/context"
    35  )
    36  
    37  var testRedisConfig = redis.Config{
    38  	ServerSettings: redis.ServerConfig{
    39  		Host: "localhost",
    40  		Port: redis.RedisPort,
    41  	},
    42  	TTL: 1 * time.Minute,
    43  }
    44  
    45  var (
    46  	rc       = redis.NewConnector(testRedisConfig, nil)
    47  	table, _ = dosa.TableFromInstance(&testentity.KeyValue{})
    48  	sr       = dosa.SchemaRef{Scope: "example", NamePrefix: "example"}
    49  	testEi   = &dosa.EntityInfo{Ref: &sr, Def: &table.EntityDefinition}
    50  )
    51  
    52  func TestUnimplementedFunctions(t *testing.T) {
    53  	err := rc.CreateIfNotExists(context.TODO(), &dosa.EntityInfo{}, nil)
    54  	assert.EqualError(t, err, new(redis.ErrNotImplemented).Error())
    55  
    56  	_, err = rc.MultiRead(context.TODO(), &dosa.EntityInfo{}, nil, dosa.All())
    57  	assert.EqualError(t, err, new(redis.ErrNotImplemented).Error())
    58  
    59  	_, err = rc.MultiUpsert(context.TODO(), &dosa.EntityInfo{}, nil)
    60  	assert.EqualError(t, err, new(redis.ErrNotImplemented).Error())
    61  
    62  	err = rc.RemoveRange(context.TODO(), &dosa.EntityInfo{}, nil)
    63  	assert.EqualError(t, err, new(redis.ErrNotImplemented).Error())
    64  
    65  	_, err = rc.MultiRemove(context.TODO(), &dosa.EntityInfo{}, nil)
    66  	assert.EqualError(t, err, new(redis.ErrNotImplemented).Error())
    67  
    68  	_, _, err = rc.Range(context.TODO(), &dosa.EntityInfo{}, nil, dosa.All(), "", 1)
    69  	assert.EqualError(t, err, new(redis.ErrNotImplemented).Error())
    70  
    71  	_, _, err = rc.Scan(context.TODO(), &dosa.EntityInfo{}, dosa.All(), "", 1)
    72  	assert.EqualError(t, err, new(redis.ErrNotImplemented).Error())
    73  }
    74  
    75  func TestWriteReadKeyValue(t *testing.T) {
    76  	if !redis.IsRunning() {
    77  		t.Skip("Redis is not running")
    78  	}
    79  
    80  	values := map[string]dosa.FieldValue{
    81  		"k": []byte{1, 2, 3},
    82  		"v": []byte{4, 5, 6},
    83  	}
    84  	err := rc.Upsert(context.TODO(), testEi, values)
    85  	assert.NoError(t, err)
    86  
    87  	readResponse, err := rc.Read(context.TODO(), testEi, map[string]dosa.FieldValue{"k": []byte{1, 2, 3}}, dosa.All())
    88  	assert.NoError(t, err)
    89  	assert.Equal(t, values, readResponse)
    90  }
    91  
    92  func TestWriteReadValueKey(t *testing.T) {
    93  	if !redis.IsRunning() {
    94  		t.Skip("Redis is not running")
    95  	}
    96  
    97  	table, _ := dosa.TableFromInstance(&testentity.ValueKey{})
    98  	sr := dosa.SchemaRef{Scope: "example", NamePrefix: "example"}
    99  	testEi := &dosa.EntityInfo{Ref: &sr, Def: &table.EntityDefinition}
   100  	values := map[string]dosa.FieldValue{
   101  		"k": []byte{1, 2, 3},
   102  		"v": []byte{4, 5, 6},
   103  	}
   104  	err := rc.Upsert(context.TODO(), testEi, values)
   105  	assert.NoError(t, err)
   106  
   107  	readResponse, err := rc.Read(context.TODO(), testEi, map[string]dosa.FieldValue{"k": []byte{1, 2, 3}}, dosa.All())
   108  	assert.NoError(t, err)
   109  	assert.Equal(t, values, readResponse)
   110  }
   111  
   112  func TestWriteValidEntity(t *testing.T) {
   113  	if !redis.IsRunning() {
   114  		t.Skip("Redis is not running")
   115  	}
   116  
   117  	err := rc.Upsert(context.TODO(), testEi, map[string]dosa.FieldValue{"k": []byte("testValue"), "v": []byte("test")})
   118  	assert.NoError(t, err)
   119  }
   120  
   121  func TestWriteNoKey(t *testing.T) {
   122  
   123  	err := rc.Upsert(context.TODO(), testEi, map[string]dosa.FieldValue{"k": []byte{}, "v": []byte("test")})
   124  	assert.Error(t, err)
   125  	assert.EqualError(t, err, "This entity schema and value not supported by redis. No key specified.")
   126  }
   127  
   128  func TestWriteNilByteValue(t *testing.T) {
   129  	if !redis.IsRunning() {
   130  		t.Skip("Redis is not running")
   131  	}
   132  
   133  	err := rc.Upsert(context.TODO(), testEi, map[string]dosa.FieldValue{"k": []byte("testValue"), "v": []byte(nil)})
   134  	assert.EqualError(t, err, "This entity schema and value not supported by redis. No value specified.")
   135  }
   136  
   137  func TestWriteNilValue(t *testing.T) {
   138  	if !redis.IsRunning() {
   139  		t.Skip("Redis is not running")
   140  	}
   141  	err := rc.Upsert(context.TODO(), testEi, map[string]dosa.FieldValue{"k": []byte("testValue"), "v": nil})
   142  	assert.EqualError(t, err, "This entity schema and value not supported by redis. No value specified.")
   143  }
   144  
   145  func TestWriteInvalidEntityKey(t *testing.T) {
   146  	table, _ := dosa.TableFromInstance(&testentity.TestEntity{})
   147  	testEi := &dosa.EntityInfo{Ref: nil, Def: &table.EntityDefinition}
   148  	err := rc.Upsert(context.TODO(), testEi, nil)
   149  	assert.EqualError(t, err, "This entity schema and value not supported by redis. Should only have a single key.")
   150  }
   151  
   152  func TestWriteInvalidEntityValue(t *testing.T) {
   153  	table, _ := dosa.TableFromInstance(&testentity.KeyValues{})
   154  	testEi := &dosa.EntityInfo{Ref: nil, Def: &table.EntityDefinition}
   155  	err := rc.Upsert(context.TODO(), testEi, nil)
   156  	assert.EqualError(t, err, "This entity schema and value not supported by redis. Should have one key, one value.")
   157  }
   158  
   159  func TestReadNotFound(t *testing.T) {
   160  	if !redis.IsRunning() {
   161  		t.Skip("Redis is not running")
   162  	}
   163  
   164  	// Expect a key not found error when reading something that does not exist in redis
   165  	randBytes := make([]byte, 10)
   166  	rand.Read(randBytes)
   167  	_, err := rc.Read(context.TODO(), testEi, map[string]dosa.FieldValue{"k": randBytes}, dosa.All())
   168  	assert.EqualError(t, err, new(dosa.ErrNotFound).Error())
   169  }
   170  
   171  func TestReadInvalidEntityKey(t *testing.T) {
   172  	table, _ := dosa.TableFromInstance(&testentity.TestEntity{})
   173  	testEi := &dosa.EntityInfo{Ref: nil, Def: &table.EntityDefinition}
   174  	_, err := rc.Read(context.TODO(), testEi, nil, dosa.All())
   175  	assert.EqualError(t, err, "This entity schema and value not supported by redis. Should only have a single key.")
   176  }
   177  
   178  func TestReadNoKey(t *testing.T) {
   179  	_, err := rc.Read(context.TODO(), testEi, map[string]dosa.FieldValue{"k": []byte{}}, dosa.All())
   180  	assert.EqualError(t, err, "This entity schema and value not supported by redis. No key specified.")
   181  }
   182  
   183  func TestReadInvalidEntityValue(t *testing.T) {
   184  	table, _ := dosa.TableFromInstance(&testentity.KeyValues{})
   185  	testEi := &dosa.EntityInfo{Ref: nil, Def: &table.EntityDefinition}
   186  	_, err := rc.Read(context.TODO(), testEi, nil, dosa.All())
   187  	assert.EqualError(t, err, "This entity schema and value not supported by redis. Should have one key, one value.")
   188  }
   189  
   190  func TestRemove(t *testing.T) {
   191  	if !redis.IsRunning() {
   192  		t.Skip("Redis is not running")
   193  	}
   194  	err := rc.Remove(context.TODO(), testEi, map[string]dosa.FieldValue{"k": []byte{1, 1}})
   195  	assert.NoError(t, err)
   196  }
   197  
   198  func TestRemoveNoKey(t *testing.T) {
   199  	err := rc.Remove(context.TODO(), testEi, map[string]dosa.FieldValue{"k": []byte(nil)})
   200  	assert.EqualError(t, err, "This entity schema and value not supported by redis. No key specified.")
   201  }
   202  
   203  func TestRemoveInvalidEntity(t *testing.T) {
   204  	table, _ := dosa.TableFromInstance(&testentity.TestEntity{})
   205  	testEi := &dosa.EntityInfo{Ref: nil, Def: &table.EntityDefinition}
   206  	err := rc.Remove(context.TODO(), testEi, nil)
   207  	assert.EqualError(t, err, "This entity schema and value not supported by redis. Should only have a single key.")
   208  }
   209  
   210  func TestEntityTypeNotBytes(t *testing.T) {
   211  	table, err := dosa.TableFromInstance(&testentity.KeyValueNonByte{})
   212  	testEi := &dosa.EntityInfo{Ref: nil, Def: &table.EntityDefinition}
   213  	_, err = rc.Read(context.TODO(), testEi, nil, dosa.All())
   214  	assert.EqualError(t, err, "This entity schema and value not supported by redis. Types should be []byte.")
   215  }
   216  
   217  func TestShutdownConnector(t *testing.T) {
   218  	var rc = redis.NewConnector(testRedisConfig, nil)
   219  	err := rc.Shutdown()
   220  	assert.NoError(t, err)
   221  	err = rc.Shutdown()
   222  	assert.NoError(t, err)
   223  }
   224  
   225  // Test recording cache hit/miss and error stats
   226  func TestLogStats(t *testing.T) {
   227  	if !redis.IsRunning() {
   228  		t.Skip("Redis is not running")
   229  	}
   230  
   231  	type testCase struct {
   232  		method      string
   233  		scenario    string
   234  		writeValues map[string]dosa.FieldValue
   235  		redisFunc   func(dosa.Connector)
   236  		config      redis.Config
   237  	}
   238  
   239  	testMethod := func(tc testCase) {
   240  		ctrl := gomock.NewController(t)
   241  		defer ctrl.Finish()
   242  		stats := mocks.NewMockScope(ctrl)
   243  
   244  		ctrl2 := gomock.NewController(t)
   245  		defer ctrl2.Finish()
   246  		counter := mocks.NewMockCounter(ctrl2)
   247  
   248  		setupStatsExpectations(stats, counter, tc.method, tc.scenario)
   249  
   250  		rc := redis.NewConnector(tc.config, stats)
   251  		rc.Upsert(context.TODO(), testEi, tc.writeValues)
   252  		tc.redisFunc(rc)
   253  	}
   254  
   255  	values := map[string]dosa.FieldValue{"k": []byte{1, 2, 3}, "v": []byte{4, 5, 6}}
   256  
   257  	testCases := []testCase{
   258  		// Test that a successful read from redis logs as cache hit
   259  		{
   260  			scenario:    "hit",
   261  			writeValues: values,
   262  			config:      testRedisConfig,
   263  			method:      "Read",
   264  			redisFunc: func(rc dosa.Connector) {
   265  				rc.Read(context.TODO(), testEi, map[string]dosa.FieldValue{"k": []byte{1, 2, 3}}, dosa.All())
   266  			},
   267  		},
   268  		// Test that a not found error is a cache miss
   269  		{
   270  			scenario:    "miss",
   271  			writeValues: nil,
   272  			config:      testRedisConfig,
   273  			method:      "Read",
   274  			redisFunc: func(rc dosa.Connector) {
   275  				rc.Read(context.TODO(), testEi, map[string]dosa.FieldValue{"k": []byte{4, 5, 6}}, dosa.All())
   276  			},
   277  		},
   278  		// Make sure we log errors for methods
   279  		{
   280  			scenario:    "error",
   281  			writeValues: nil,
   282  			config:      redis.Config{},
   283  			method:      "Read",
   284  			redisFunc:   func(rc dosa.Connector) { rc.Read(context.TODO(), testEi, values, dosa.All()) },
   285  		},
   286  		{
   287  			scenario:    "error",
   288  			writeValues: nil,
   289  			config:      redis.Config{},
   290  			method:      "Remove",
   291  			redisFunc:   func(rc dosa.Connector) { rc.Remove(context.TODO(), testEi, values) },
   292  		},
   293  		{
   294  			scenario:    "error",
   295  			writeValues: nil,
   296  			config:      redis.Config{},
   297  			method:      "Upsert",
   298  			redisFunc:   func(rc dosa.Connector) { rc.Upsert(context.TODO(), testEi, values) },
   299  		},
   300  	}
   301  	for _, t := range testCases {
   302  		testMethod(t)
   303  	}
   304  }
   305  
   306  func setupStatsExpectations(stats *mocks.MockScope, counter *mocks.MockCounter, method, action string) {
   307  	stats.EXPECT().SubScope("cache").Return(stats)
   308  	stats.EXPECT().Tagged(map[string]string{"method": method}).Return(stats)
   309  	stats.EXPECT().Counter(action).Return(counter)
   310  	counter.EXPECT().Inc(int64(1))
   311  }