package replay import ( "context" "errors" "net" "testing" "time" "galaxy/gateway/internal/config" "github.com/alicebob/miniredis/v2" "github.com/redis/go-redis/v9" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func newRedisClient(t *testing.T, addr string) *redis.Client { t.Helper() client := redis.NewClient(&redis.Options{ Addr: addr, Protocol: 2, DisableIdentity: true, }) t.Cleanup(func() { assert.NoError(t, client.Close()) }) return client } func TestNewRedisStore(t *testing.T) { t.Parallel() server := miniredis.RunT(t) client := newRedisClient(t, server.Addr()) validCfg := config.ReplayRedisConfig{ KeyPrefix: "gateway:replay:", ReserveTimeout: 250 * time.Millisecond, } tests := []struct { name string client *redis.Client cfg config.ReplayRedisConfig wantErr string }{ {name: "valid config", client: client, cfg: validCfg}, {name: "nil client", client: nil, cfg: validCfg, wantErr: "nil redis client"}, { name: "empty replay key prefix", client: client, cfg: config.ReplayRedisConfig{ReserveTimeout: 250 * time.Millisecond}, wantErr: "replay key prefix must not be empty", }, { name: "non-positive reserve timeout", client: client, cfg: config.ReplayRedisConfig{KeyPrefix: "gateway:replay:"}, wantErr: "reserve timeout must be positive", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() store, err := NewRedisStore(tt.client, tt.cfg) if tt.wantErr != "" { require.Error(t, err) require.ErrorContains(t, err, tt.wantErr) return } require.NoError(t, err) require.NotNil(t, store) }) } } func TestRedisStoreReserve(t *testing.T) { t.Parallel() tests := []struct { name string replayCfg config.ReplayRedisConfig deviceSessionID string requestID string ttl time.Duration secondReserve func(*testing.T, Store) wantErrIs error wantErrText string }{ { name: "first reservation succeeds", deviceSessionID: "device-session-123", requestID: "request-123", ttl: 5 * time.Second, }, { name: "duplicate reservation is rejected", deviceSessionID: "device-session-123", requestID: "request-123", ttl: 5 * time.Second, secondReserve: func(t *testing.T, store Store) { t.Helper() err := store.Reserve(context.Background(), "device-session-123", "request-123", 5*time.Second) require.ErrorIs(t, err, ErrDuplicate) }, }, { name: "same request id in distinct sessions does not collide", deviceSessionID: "device-session-123", requestID: "request-123", ttl: 5 * time.Second, secondReserve: func(t *testing.T, store Store) { t.Helper() require.NoError(t, store.Reserve(context.Background(), "device-session-456", "request-123", 5*time.Second)) }, }, { name: "empty device session id", requestID: "request-123", ttl: 5 * time.Second, wantErrText: "empty device session id", }, { name: "empty request id", deviceSessionID: "device-session-123", ttl: 5 * time.Second, wantErrText: "empty request id", }, { name: "non-positive ttl", deviceSessionID: "device-session-123", requestID: "request-123", wantErrText: "ttl must be positive", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() server := miniredis.RunT(t) store := newTestRedisStore(t, server, tt.replayCfg) err := store.Reserve(context.Background(), tt.deviceSessionID, tt.requestID, tt.ttl) if tt.wantErrIs != nil || tt.wantErrText != "" { require.Error(t, err) if tt.wantErrIs != nil { require.ErrorIs(t, err, tt.wantErrIs) } if tt.wantErrText != "" { require.ErrorContains(t, err, tt.wantErrText) } return } require.NoError(t, err) if tt.secondReserve != nil { tt.secondReserve(t, store) } }) } } func TestRedisStoreReserveReturnsBackendError(t *testing.T) { t.Parallel() client := newRedisClient(t, unusedTCPAddr(t)) store, err := NewRedisStore(client, config.ReplayRedisConfig{ KeyPrefix: "gateway:replay:", ReserveTimeout: 100 * time.Millisecond, }) require.NoError(t, err) err = store.Reserve(context.Background(), "device-session-123", "request-123", 5*time.Second) require.Error(t, err) assert.False(t, errors.Is(err, ErrDuplicate)) assert.ErrorContains(t, err, "reserve replay request in redis") } func newTestRedisStore(t *testing.T, server *miniredis.Miniredis, replayCfg config.ReplayRedisConfig) *RedisStore { t.Helper() if replayCfg.KeyPrefix == "" { replayCfg.KeyPrefix = "gateway:replay:" } if replayCfg.ReserveTimeout == 0 { replayCfg.ReserveTimeout = 250 * time.Millisecond } store, err := NewRedisStore(newRedisClient(t, server.Addr()), replayCfg) require.NoError(t, err) return store } func unusedTCPAddr(t *testing.T) string { t.Helper() listener, err := net.Listen("tcp", "127.0.0.1:0") require.NoError(t, err) addr := listener.Addr().String() require.NoError(t, listener.Close()) return addr }