github.com/grafana/synthetic-monitoring-agent@v0.64.2/internal/cache/memcached_test.go (about) 1 package cache 2 3 import ( 4 "context" 5 "io" 6 "testing" 7 "time" 8 9 "github.com/rs/zerolog" 10 "github.com/stretchr/testify/require" 11 ) 12 13 func testLogger() zerolog.Logger { 14 return zerolog.New(io.Discard) 15 } 16 17 func TestNew(t *testing.T) { 18 t.Run("valid configuration", func(t *testing.T) { 19 cache, err := NewMemcachedClient(t.Context(), MemcachedConfig{ 20 Servers: []string{"localhost:11211"}, 21 Logger: testLogger(), 22 }) 23 require.NoError(t, err) 24 require.NotNil(t, cache) 25 require.Implements(t, (*Cache)(nil), cache) 26 }) 27 28 t.Run("multiple servers", func(t *testing.T) { 29 client, err := NewMemcachedClient(t.Context(), MemcachedConfig{ 30 Servers: []string{"localhost:11211", "cache1:11211", "cache2:11211"}, 31 Logger: testLogger(), 32 }) 33 require.NoError(t, err) 34 require.NotNil(t, client) 35 }) 36 37 t.Run("custom timeout and max idle conns", func(t *testing.T) { 38 cache, err := NewMemcachedClient(t.Context(), MemcachedConfig{ 39 Servers: []string{"localhost:11211"}, 40 Logger: testLogger(), 41 Timeout: 200 * time.Millisecond, 42 MaxIdleConns: 5, 43 }) 44 require.NoError(t, err) 45 require.NotNil(t, cache) 46 47 require.Equal(t, 200*time.Millisecond, cache.mc.Timeout) 48 require.Equal(t, 5, cache.mc.MaxIdleConns) 49 }) 50 51 t.Run("default timeout and max idle conns", func(t *testing.T) { 52 cache, err := NewMemcachedClient(t.Context(), MemcachedConfig{ 53 Servers: []string{"localhost:11211"}, 54 Logger: testLogger(), 55 }) 56 require.NoError(t, err) 57 58 require.Equal(t, DefaultTimeout, cache.mc.Timeout) 59 require.Equal(t, DefaultMaxIdleConns, cache.mc.MaxIdleConns) 60 }) 61 62 t.Run("servers with whitespace", func(t *testing.T) { 63 client, err := NewMemcachedClient(t.Context(), MemcachedConfig{ 64 Servers: []string{" localhost:11211 ", " cache1:11211 "}, 65 Logger: testLogger(), 66 }) 67 require.NoError(t, err) 68 require.NotNil(t, client) 69 }) 70 } 71 72 func TestNewErrors(t *testing.T) { 73 t.Run("no servers", func(t *testing.T) { 74 client, err := NewMemcachedClient(t.Context(), MemcachedConfig{ 75 Servers: []string{}, 76 Logger: testLogger(), 77 }) 78 require.Error(t, err) 79 require.Nil(t, client) 80 require.Contains(t, err.Error(), "at least one memcached server must be provided") 81 }) 82 83 t.Run("empty server address", func(t *testing.T) { 84 client, err := NewMemcachedClient(t.Context(), MemcachedConfig{ 85 Servers: []string{"localhost:11211", "", "cache1:11211"}, 86 Logger: testLogger(), 87 }) 88 require.Error(t, err) 89 require.Nil(t, client) 90 require.Contains(t, err.Error(), "server address at index 1 is empty") 91 }) 92 93 t.Run("invalid server address - no port", func(t *testing.T) { 94 client, err := NewMemcachedClient(t.Context(), MemcachedConfig{ 95 Servers: []string{"localhost"}, 96 Logger: testLogger(), 97 }) 98 require.Error(t, err) 99 require.Nil(t, client) 100 require.Contains(t, err.Error(), "invalid server address") 101 }) 102 103 t.Run("invalid server address - bad format", func(t *testing.T) { 104 client, err := NewMemcachedClient(t.Context(), MemcachedConfig{ 105 Servers: []string{"not a valid address"}, 106 Logger: testLogger(), 107 }) 108 require.Error(t, err) 109 require.Nil(t, client) 110 require.Contains(t, err.Error(), "invalid server address") 111 }) 112 } 113 114 func TestValidateKey(t *testing.T) { 115 t.Run("valid key", func(t *testing.T) { 116 err := validateKey("valid:key:123") 117 require.NoError(t, err) 118 }) 119 120 t.Run("empty key", func(t *testing.T) { 121 err := validateKey("") 122 require.Error(t, err) 123 require.Contains(t, err.Error(), "key cannot be empty") 124 }) 125 126 t.Run("key too long", func(t *testing.T) { 127 longKey := string(make([]byte, MaxKeyLength+1)) 128 err := validateKey(longKey) 129 require.Error(t, err) 130 require.Contains(t, err.Error(), "key length") 131 require.Contains(t, err.Error(), "exceeds maximum") 132 }) 133 134 t.Run("key at max length", func(t *testing.T) { 135 maxKey := string(make([]byte, MaxKeyLength)) 136 err := validateKey(maxKey) 137 require.NoError(t, err) 138 }) 139 } 140 141 func TestSetGetDelete(t *testing.T) { 142 // These tests require a running memcached instance 143 // They will fail gracefully if memcached is not available 144 if testing.Short() { 145 t.Skip("skipping integration test in short mode") 146 } 147 148 client, err := NewMemcachedClient(t.Context(), MemcachedConfig{ 149 Servers: []string{"localhost:11211"}, 150 Logger: testLogger(), 151 }) 152 require.NoError(t, err) 153 154 err = client.mc.Ping() 155 if err != nil { 156 t.Skipf("memcached not available: %v", err) 157 } 158 159 ctx := context.Background() 160 161 t.Run("set and get string", func(t *testing.T) { 162 key := "test:string" 163 value := "hello world" 164 165 err := client.Set(ctx, key, value, 1*time.Minute) 166 require.NoError(t, err) 167 168 var result string 169 170 err = client.Get(ctx, key, &result) 171 require.NoError(t, err) 172 require.Equal(t, value, result) 173 174 // Clean up 175 err = client.Delete(ctx, key) 176 require.NoError(t, err) 177 }) 178 179 t.Run("set and get struct", func(t *testing.T) { 180 key := "test:struct" 181 value := TestStruct{ 182 ID: 123, 183 Name: "test", 184 Tags: []string{"a", "b", "c"}, 185 } 186 187 err := client.Set(ctx, key, value, 1*time.Minute) 188 require.NoError(t, err) 189 190 var result TestStruct 191 192 err = client.Get(ctx, key, &result) 193 require.NoError(t, err) 194 require.Equal(t, value, result) 195 196 // Clean up 197 err = client.Delete(ctx, key) 198 require.NoError(t, err) 199 }) 200 201 t.Run("get non-existent key", func(t *testing.T) { 202 key := "test:nonexistent" 203 204 var result string 205 206 err := client.Get(ctx, key, &result) 207 require.ErrorIs(t, err, ErrCacheMiss) 208 require.Empty(t, result) 209 }) 210 211 t.Run("delete non-existent key", func(t *testing.T) { 212 key := "test:nonexistent:delete" 213 214 err := client.Delete(ctx, key) 215 require.NoError(t, err) // Deleting non-existent key is not an error 216 }) 217 218 t.Run("set with zero expiration", func(t *testing.T) { 219 key := "test:no:expiration" 220 value := "no expiration" 221 222 err := client.Set(ctx, key, value, 0) 223 require.NoError(t, err) 224 225 var result string 226 227 err = client.Get(ctx, key, &result) 228 require.NoError(t, err) 229 require.Equal(t, value, result) 230 231 // Clean up 232 err = client.Delete(ctx, key) 233 require.NoError(t, err) 234 }) 235 } 236 237 func TestSetErrors(t *testing.T) { 238 client, err := NewMemcachedClient(t.Context(), MemcachedConfig{ 239 Servers: []string{"localhost:11211"}, 240 Logger: testLogger(), 241 }) 242 require.NoError(t, err) 243 244 ctx := context.Background() 245 246 t.Run("empty key", func(t *testing.T) { 247 err := client.Set(ctx, "", "value", 1*time.Minute) 248 require.Error(t, err) 249 require.Contains(t, err.Error(), "key cannot be empty") 250 }) 251 252 t.Run("key too long", func(t *testing.T) { 253 longKey := string(make([]byte, MaxKeyLength+1)) 254 err := client.Set(ctx, longKey, "value", 1*time.Minute) 255 require.Error(t, err) 256 require.Contains(t, err.Error(), "key length") 257 }) 258 259 t.Run("un-encodable value", func(t *testing.T) { 260 err := client.Set(ctx, "test:channel", make(chan int), 1*time.Minute) 261 require.Error(t, err) 262 require.Contains(t, err.Error(), "failed to encode value") 263 }) 264 } 265 266 func TestGetErrors(t *testing.T) { 267 client, err := NewMemcachedClient(t.Context(), MemcachedConfig{ 268 Servers: []string{"localhost:11211"}, 269 Logger: testLogger(), 270 }) 271 require.NoError(t, err) 272 273 ctx := context.Background() 274 275 t.Run("empty key", func(t *testing.T) { 276 var result string 277 278 err := client.Get(ctx, "", &result) 279 require.Error(t, err) 280 require.NotErrorIs(t, err, ErrCacheMiss) 281 require.Contains(t, err.Error(), "key cannot be empty") 282 }) 283 284 t.Run("key too long", func(t *testing.T) { 285 longKey := string(make([]byte, MaxKeyLength+1)) 286 287 var result string 288 289 err := client.Get(ctx, longKey, &result) 290 require.Error(t, err) 291 require.NotErrorIs(t, err, ErrCacheMiss) 292 require.Contains(t, err.Error(), "key length") 293 }) 294 } 295 296 func TestDeleteErrors(t *testing.T) { 297 client, err := NewMemcachedClient(t.Context(), MemcachedConfig{ 298 Servers: []string{"localhost:11211"}, 299 Logger: testLogger(), 300 }) 301 require.NoError(t, err) 302 303 ctx := context.Background() 304 305 t.Run("empty key", func(t *testing.T) { 306 err := client.Delete(ctx, "") 307 require.Error(t, err) 308 require.Contains(t, err.Error(), "key cannot be empty") 309 }) 310 311 t.Run("key too long", func(t *testing.T) { 312 longKey := string(make([]byte, MaxKeyLength+1)) 313 err := client.Delete(ctx, longKey) 314 require.Error(t, err) 315 require.Contains(t, err.Error(), "key length") 316 }) 317 }