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 }