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  }