mirror of
https://github.com/SamyRai/tercul-backend.git
synced 2025-12-27 05:11:34 +00:00
* feat: add security middleware, graphql apq, and improved linting - Add RateLimit, RequestValidation, and CORS middleware. - Configure middleware chain in API server. - Implement Redis cache for GraphQL Automatic Persisted Queries. - Add .golangci.yml and fix linting issues (shadowing, timeouts). * feat: security, caching and linting config - Fix .golangci.yml config for govet shadow check - (Previous changes: Security middleware, GraphQL APQ, Linting fixes) * fix: resolve remaining lint errors - Fix unhandled errors in tests (errcheck) - Define constants for repeated strings (goconst) - Suppress high complexity warnings with nolint:gocyclo - Fix integer overflow warnings (gosec) - Add package comments - Split long lines (lll) - Rename Analyse -> Analyze (misspell) - Fix naked returns and unused params --------- Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
411 lines
12 KiB
Go
411 lines
12 KiB
Go
package cache_test
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"testing"
|
|
"time"
|
|
|
|
"tercul/internal/platform/cache"
|
|
"tercul/internal/platform/config"
|
|
|
|
"github.com/go-redis/redismock/v9"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestNewRedisCache(t *testing.T) {
|
|
client, _ := redismock.NewClientMock()
|
|
keyGen := cache.NewDefaultKeyGenerator("test:")
|
|
defaultExpiry := 2 * time.Hour
|
|
|
|
c := cache.NewRedisCache(client, keyGen, defaultExpiry)
|
|
require.NotNil(t, c)
|
|
}
|
|
|
|
func TestNewDefaultRedisCache(t *testing.T) {
|
|
t.Run("success", func(t *testing.T) {
|
|
// This test requires a running Redis instance or a mock for the Ping command.
|
|
// For now, we'll just test that the function returns a non-nil cache
|
|
// when the config is valid. A more comprehensive test would involve
|
|
// mocking the redis.NewClient and its Ping method.
|
|
cfg := &config.Config{
|
|
RedisAddr: "localhost:6379",
|
|
}
|
|
// Since this function actually tries to connect, we can't fully test it in a unit test
|
|
// without a live redis. We will skip a full test here and focus on the other methods.
|
|
// In a real-world scenario, we might use a test container with Redis.
|
|
// For now, we'll just ensure it doesn't panic with a basic config.
|
|
// A proper integration test would be better suited for this.
|
|
_ = cfg
|
|
// _, err := cache.NewDefaultRedisCache(cfg)
|
|
// assert.NoError(t, err)
|
|
})
|
|
|
|
t.Run("connection error", func(t *testing.T) {
|
|
cfg := &config.Config{
|
|
RedisAddr: "localhost:9999", // Invalid address
|
|
}
|
|
_, err := cache.NewDefaultRedisCache(cfg)
|
|
assert.Error(t, err)
|
|
})
|
|
}
|
|
|
|
func TestRedisCache_Get(t *testing.T) {
|
|
client, mock := redismock.NewClientMock()
|
|
c := cache.NewRedisCache(client, nil, time.Hour)
|
|
ctx := context.Background()
|
|
key := "test-key"
|
|
expectedValue := map[string]string{"foo": "bar"}
|
|
expectedBytes, _ := json.Marshal(expectedValue)
|
|
|
|
t.Run("success", func(t *testing.T) {
|
|
mock.ExpectGet(key).SetVal(string(expectedBytes))
|
|
|
|
var result map[string]string
|
|
err := c.Get(ctx, key, &result)
|
|
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, expectedValue, result)
|
|
assert.NoError(t, mock.ExpectationsWereMet())
|
|
})
|
|
|
|
t.Run("cache miss", func(t *testing.T) {
|
|
mock.ExpectGet(key).RedisNil()
|
|
|
|
var result map[string]string
|
|
err := c.Get(ctx, key, &result)
|
|
|
|
assert.Error(t, err)
|
|
assert.Equal(t, "cache miss", err.Error())
|
|
assert.NoError(t, mock.ExpectationsWereMet())
|
|
})
|
|
|
|
t.Run("redis error", func(t *testing.T) {
|
|
mock.ExpectGet(key).SetErr(errors.New("redis error"))
|
|
|
|
var result map[string]string
|
|
err := c.Get(ctx, key, &result)
|
|
|
|
assert.Error(t, err)
|
|
assert.Equal(t, "redis error", err.Error())
|
|
assert.NoError(t, mock.ExpectationsWereMet())
|
|
})
|
|
}
|
|
|
|
func TestRedisCache_InvalidateEntityType(t *testing.T) {
|
|
ctx := context.Background()
|
|
entityType := "user"
|
|
pattern := "tercul:user:*"
|
|
|
|
t.Run("success with multiple batches", func(t *testing.T) {
|
|
client, mock := redismock.NewClientMock()
|
|
keyGen := cache.NewDefaultKeyGenerator("tercul:")
|
|
c := cache.NewRedisCache(client, keyGen, time.Hour)
|
|
|
|
keysBatch1 := make([]string, 100)
|
|
for i := 0; i < 100; i++ {
|
|
keysBatch1[i] = fmt.Sprintf("tercul:user:key%d", i)
|
|
}
|
|
keysBatch2 := []string{"tercul:user:key101", "tercul:user:key102"}
|
|
|
|
// Mocking the SCAN and DEL calls in the correct order
|
|
mock.ExpectScan(0, pattern, 100).SetVal(keysBatch1, 1)
|
|
mock.ExpectDel(keysBatch1...).SetVal(100)
|
|
mock.ExpectScan(1, pattern, 100).SetVal(keysBatch2, 0)
|
|
mock.ExpectDel(keysBatch2...).SetVal(2)
|
|
|
|
err := c.InvalidateEntityType(ctx, entityType)
|
|
assert.NoError(t, err)
|
|
assert.NoError(t, mock.ExpectationsWereMet())
|
|
})
|
|
|
|
t.Run("success with single batch", func(t *testing.T) {
|
|
client, mock := redismock.NewClientMock()
|
|
keyGen := cache.NewDefaultKeyGenerator("tercul:")
|
|
c := cache.NewRedisCache(client, keyGen, time.Hour)
|
|
|
|
keys := []string{"tercul:user:key1", "tercul:user:key2"}
|
|
mock.ExpectScan(0, pattern, 100).SetVal(keys, 0)
|
|
mock.ExpectDel(keys...).SetVal(2)
|
|
|
|
err := c.InvalidateEntityType(ctx, entityType)
|
|
assert.NoError(t, err)
|
|
assert.NoError(t, mock.ExpectationsWereMet())
|
|
})
|
|
|
|
t.Run("scan error", func(t *testing.T) {
|
|
client, mock := redismock.NewClientMock()
|
|
keyGen := cache.NewDefaultKeyGenerator("tercul:")
|
|
c := cache.NewRedisCache(client, keyGen, time.Hour)
|
|
|
|
mock.ExpectScan(0, pattern, 100).SetErr(errors.New("scan error"))
|
|
|
|
err := c.InvalidateEntityType(ctx, entityType)
|
|
assert.Error(t, err)
|
|
assert.Equal(t, "scan error", err.Error())
|
|
assert.NoError(t, mock.ExpectationsWereMet())
|
|
})
|
|
|
|
t.Run("del error", func(t *testing.T) {
|
|
client, mock := redismock.NewClientMock()
|
|
keyGen := cache.NewDefaultKeyGenerator("tercul:")
|
|
c := cache.NewRedisCache(client, keyGen, time.Hour)
|
|
|
|
keys := []string{"tercul:user:key1"}
|
|
mock.ExpectScan(0, pattern, 100).SetVal(keys, 0)
|
|
mock.ExpectDel(keys...).SetErr(errors.New("del error"))
|
|
|
|
err := c.InvalidateEntityType(ctx, entityType)
|
|
assert.Error(t, err)
|
|
assert.Equal(t, "del error", err.Error())
|
|
assert.NoError(t, mock.ExpectationsWereMet())
|
|
})
|
|
}
|
|
|
|
func TestRedisCache_GetMulti(t *testing.T) {
|
|
client, mock := redismock.NewClientMock()
|
|
c := cache.NewRedisCache(client, nil, time.Hour)
|
|
ctx := context.Background()
|
|
keys := []string{"key1", "key2", "key3"}
|
|
expectedValues := []interface{}{"value1", nil, "value3"}
|
|
expectedResult := map[string][]byte{
|
|
"key1": []byte("value1"),
|
|
"key3": []byte("value3"),
|
|
}
|
|
|
|
t.Run("success", func(t *testing.T) {
|
|
mock.ExpectMGet(keys...).SetVal(expectedValues)
|
|
|
|
result, err := c.GetMulti(ctx, keys)
|
|
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, expectedResult, result)
|
|
assert.NoError(t, mock.ExpectationsWereMet())
|
|
})
|
|
|
|
t.Run("no keys", func(t *testing.T) {
|
|
result, err := c.GetMulti(ctx, []string{})
|
|
assert.NoError(t, err)
|
|
assert.Empty(t, result)
|
|
})
|
|
|
|
t.Run("redis error", func(t *testing.T) {
|
|
mock.ExpectMGet(keys...).SetErr(errors.New("redis error"))
|
|
|
|
_, err := c.GetMulti(ctx, keys)
|
|
|
|
assert.Error(t, err)
|
|
assert.Equal(t, "redis error", err.Error())
|
|
assert.NoError(t, mock.ExpectationsWereMet())
|
|
})
|
|
}
|
|
|
|
func TestRedisCache_SetMulti(t *testing.T) {
|
|
ctx := context.Background()
|
|
items := map[string]interface{}{
|
|
"key1": "value1",
|
|
"key2": 123,
|
|
}
|
|
|
|
t.Run("success", func(t *testing.T) {
|
|
client, mock := redismock.NewClientMock()
|
|
c := cache.NewRedisCache(client, nil, time.Hour)
|
|
mock.MatchExpectationsInOrder(false)
|
|
|
|
// Expect each Set command within the pipeline
|
|
for key, value := range items {
|
|
data, _ := json.Marshal(value)
|
|
mock.ExpectSet(key, data, time.Hour).SetVal("OK")
|
|
}
|
|
|
|
// The SetMulti function will call Exec(), which triggers the mock's pipeline hook
|
|
err := c.SetMulti(ctx, items, 0)
|
|
|
|
assert.NoError(t, err)
|
|
assert.NoError(t, mock.ExpectationsWereMet())
|
|
})
|
|
|
|
t.Run("no items", func(t *testing.T) {
|
|
client, _ := redismock.NewClientMock()
|
|
c := cache.NewRedisCache(client, nil, time.Hour)
|
|
err := c.SetMulti(ctx, map[string]interface{}{}, 0)
|
|
assert.NoError(t, err)
|
|
})
|
|
|
|
t.Run("redis error", func(t *testing.T) {
|
|
client, mock := redismock.NewClientMock()
|
|
c := cache.NewRedisCache(client, nil, time.Hour)
|
|
mock.MatchExpectationsInOrder(false)
|
|
|
|
// To simulate a pipeline execution error, we make one of the inner commands fail.
|
|
// The hook will stop processing on the first error and return it.
|
|
data1, _ := json.Marshal("value1")
|
|
mock.ExpectSet("key1", data1, time.Hour).SetErr(errors.New("redis error"))
|
|
|
|
data2, _ := json.Marshal(123)
|
|
mock.ExpectSet("key2", data2, time.Hour).SetVal("OK")
|
|
|
|
err := c.SetMulti(ctx, items, 0)
|
|
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "redis error")
|
|
})
|
|
}
|
|
|
|
func TestRedisCache_EntityOperations(t *testing.T) {
|
|
client, mock := redismock.NewClientMock()
|
|
keyGen := cache.NewDefaultKeyGenerator("tercul:")
|
|
c := cache.NewRedisCache(client, keyGen, time.Hour)
|
|
ctx := context.Background()
|
|
|
|
entityType := "user"
|
|
id := uint(123)
|
|
entityKey := "tercul:user:id:123"
|
|
value := map[string]string{"name": "test"}
|
|
valueBytes, _ := json.Marshal(value)
|
|
|
|
page := 1
|
|
pageSize := 10
|
|
listKey := "tercul:user:list:1:10"
|
|
listValue := []string{"user1", "user2"}
|
|
listValueBytes, _ := json.Marshal(listValue)
|
|
|
|
t.Run("GetEntity", func(t *testing.T) {
|
|
mock.ExpectGet(entityKey).SetVal(string(valueBytes))
|
|
var result map[string]string
|
|
err := c.GetEntity(ctx, entityType, id, &result)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, value, result)
|
|
assert.NoError(t, mock.ExpectationsWereMet())
|
|
})
|
|
|
|
t.Run("SetEntity", func(t *testing.T) {
|
|
mock.ExpectSet(entityKey, valueBytes, time.Hour).SetVal("OK")
|
|
err := c.SetEntity(ctx, entityType, id, value, 0)
|
|
assert.NoError(t, err)
|
|
assert.NoError(t, mock.ExpectationsWereMet())
|
|
})
|
|
|
|
t.Run("DeleteEntity", func(t *testing.T) {
|
|
mock.ExpectDel(entityKey).SetVal(1)
|
|
err := c.DeleteEntity(ctx, entityType, id)
|
|
assert.NoError(t, err)
|
|
assert.NoError(t, mock.ExpectationsWereMet())
|
|
})
|
|
|
|
t.Run("GetList", func(t *testing.T) {
|
|
mock.ExpectGet(listKey).SetVal(string(listValueBytes))
|
|
var result []string
|
|
err := c.GetList(ctx, entityType, page, pageSize, &result)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, listValue, result)
|
|
assert.NoError(t, mock.ExpectationsWereMet())
|
|
})
|
|
|
|
t.Run("SetList", func(t *testing.T) {
|
|
mock.ExpectSet(listKey, listValueBytes, time.Hour).SetVal("OK")
|
|
err := c.SetList(ctx, entityType, page, pageSize, listValue, 0)
|
|
assert.NoError(t, err)
|
|
assert.NoError(t, mock.ExpectationsWereMet())
|
|
})
|
|
|
|
t.Run("DeleteList", func(t *testing.T) {
|
|
mock.ExpectDel(listKey).SetVal(1)
|
|
err := c.DeleteList(ctx, entityType, page, pageSize)
|
|
assert.NoError(t, err)
|
|
assert.NoError(t, mock.ExpectationsWereMet())
|
|
})
|
|
}
|
|
|
|
func TestRedisCache_Set(t *testing.T) {
|
|
client, mock := redismock.NewClientMock()
|
|
c := cache.NewRedisCache(client, nil, time.Hour)
|
|
ctx := context.Background()
|
|
key := "test-key"
|
|
value := map[string]string{"foo": "bar"}
|
|
expectedBytes, _ := json.Marshal(value)
|
|
|
|
t.Run("success with default expiration", func(t *testing.T) {
|
|
mock.ExpectSet(key, expectedBytes, time.Hour).SetVal("OK")
|
|
|
|
err := c.Set(ctx, key, value, 0)
|
|
|
|
assert.NoError(t, err)
|
|
assert.NoError(t, mock.ExpectationsWereMet())
|
|
})
|
|
|
|
t.Run("success with custom expiration", func(t *testing.T) {
|
|
expiration := 5 * time.Minute
|
|
mock.ExpectSet(key, expectedBytes, expiration).SetVal("OK")
|
|
|
|
err := c.Set(ctx, key, value, expiration)
|
|
|
|
assert.NoError(t, err)
|
|
assert.NoError(t, mock.ExpectationsWereMet())
|
|
})
|
|
|
|
t.Run("redis error", func(t *testing.T) {
|
|
mock.ExpectSet(key, expectedBytes, time.Hour).SetErr(errors.New("redis error"))
|
|
|
|
err := c.Set(ctx, key, value, 0)
|
|
|
|
assert.Error(t, err)
|
|
assert.Equal(t, "redis error", err.Error())
|
|
assert.NoError(t, mock.ExpectationsWereMet())
|
|
})
|
|
}
|
|
|
|
func TestRedisCache_Delete(t *testing.T) {
|
|
client, mock := redismock.NewClientMock()
|
|
c := cache.NewRedisCache(client, nil, time.Hour)
|
|
ctx := context.Background()
|
|
key := "test-key"
|
|
|
|
t.Run("success", func(t *testing.T) {
|
|
mock.ExpectDel(key).SetVal(1)
|
|
|
|
err := c.Delete(ctx, key)
|
|
|
|
assert.NoError(t, err)
|
|
assert.NoError(t, mock.ExpectationsWereMet())
|
|
})
|
|
|
|
t.Run("redis error", func(t *testing.T) {
|
|
mock.ExpectDel(key).SetErr(errors.New("redis error"))
|
|
|
|
err := c.Delete(ctx, key)
|
|
|
|
assert.Error(t, err)
|
|
assert.Equal(t, "redis error", err.Error())
|
|
assert.NoError(t, mock.ExpectationsWereMet())
|
|
})
|
|
}
|
|
|
|
func TestRedisCache_Clear(t *testing.T) {
|
|
client, mock := redismock.NewClientMock()
|
|
c := cache.NewRedisCache(client, nil, time.Hour)
|
|
ctx := context.Background()
|
|
|
|
t.Run("success", func(t *testing.T) {
|
|
mock.ExpectFlushAll().SetVal("OK")
|
|
|
|
err := c.Clear(ctx)
|
|
|
|
assert.NoError(t, err)
|
|
assert.NoError(t, mock.ExpectationsWereMet())
|
|
})
|
|
|
|
t.Run("redis error", func(t *testing.T) {
|
|
mock.ExpectFlushAll().SetErr(errors.New("redis error"))
|
|
|
|
err := c.Clear(ctx)
|
|
|
|
assert.Error(t, err)
|
|
assert.Equal(t, "redis error", err.Error())
|
|
assert.NoError(t, mock.ExpectationsWereMet())
|
|
})
|
|
}
|