1
0
mirror of https://github.com/redis/go-redis.git synced 2025-07-20 22:42:59 +03:00
Files
go-redis/push_notification_coverage_test.go
Nedyalko Dyakov 03bfd9ffcc feat: remove GetRegistry from PushNotificationProcessorInterface for better encapsulation
- Remove GetRegistry() method from PushNotificationProcessorInterface
- Enforce use of GetHandler() method for cleaner API design
- Add GetRegistryForTesting() method for test access only
- Update all tests to use new testing helper methods
- Maintain clean separation between public API and internal implementation

Benefits:
- Better encapsulation - no direct registry access from public interface
- Cleaner API - forces use of GetHandler() for specific handler access
- Consistent interface design across all processor types
- Internal registry access only available for testing purposes
- Prevents misuse of registry in production code
2025-06-27 14:31:36 +03:00

454 lines
14 KiB
Go

package redis
import (
"bytes"
"context"
"net"
"testing"
"time"
"github.com/redis/go-redis/v9/internal/pool"
"github.com/redis/go-redis/v9/internal/proto"
)
// Helper function to access registry for testing
func getRegistryForTestingCoverage(processor PushNotificationProcessorInterface) *PushNotificationRegistry {
switch p := processor.(type) {
case *PushNotificationProcessor:
return p.GetRegistryForTesting()
case *VoidPushNotificationProcessor:
return p.GetRegistryForTesting()
default:
return nil
}
}
// testHandler is a simple implementation of PushNotificationHandler for testing
type testHandler struct {
handlerFunc func(ctx context.Context, notification []interface{}) bool
}
func (h *testHandler) HandlePushNotification(ctx context.Context, notification []interface{}) bool {
return h.handlerFunc(ctx, notification)
}
// newTestHandler creates a test handler from a function
func newTestHandler(f func(ctx context.Context, notification []interface{}) bool) *testHandler {
return &testHandler{handlerFunc: f}
}
// TestConnectionPoolPushNotificationIntegration tests the connection pool's
// integration with push notifications for 100% coverage.
func TestConnectionPoolPushNotificationIntegration(t *testing.T) {
// Create client with push notifications
client := NewClient(&Options{
Addr: "localhost:6379",
Protocol: 3,
PushNotifications: true,
})
defer client.Close()
processor := client.GetPushNotificationProcessor()
if processor == nil {
t.Fatal("Push notification processor should be available")
}
// Test that connections get the processor assigned
ctx := context.Background()
connPool := client.Pool().(*pool.ConnPool)
// Get a connection and verify it has the processor
cn, err := connPool.Get(ctx)
if err != nil {
t.Fatalf("Failed to get connection: %v", err)
}
defer connPool.Put(ctx, cn)
if cn.PushNotificationProcessor == nil {
t.Error("Connection should have push notification processor assigned")
}
// Connection should have a processor (no need to check IsEnabled anymore)
// Test ProcessPendingNotifications method
emptyReader := proto.NewReader(bytes.NewReader([]byte{}))
err = cn.PushNotificationProcessor.ProcessPendingNotifications(ctx, emptyReader)
if err != nil {
t.Errorf("ProcessPendingNotifications should not error with empty reader: %v", err)
}
}
// TestConnectionPoolPutWithBufferedData tests the pool's Put method
// when connections have buffered data (push notifications).
func TestConnectionPoolPutWithBufferedData(t *testing.T) {
// Create client with push notifications
client := NewClient(&Options{
Addr: "localhost:6379",
Protocol: 3,
PushNotifications: true,
})
defer client.Close()
ctx := context.Background()
connPool := client.Pool().(*pool.ConnPool)
// Get a connection
cn, err := connPool.Get(ctx)
if err != nil {
t.Fatalf("Failed to get connection: %v", err)
}
// Verify connection has processor
if cn.PushNotificationProcessor == nil {
t.Error("Connection should have push notification processor")
}
// Test putting connection back (should not panic or error)
connPool.Put(ctx, cn)
// Get another connection to verify pool operations work
cn2, err := connPool.Get(ctx)
if err != nil {
t.Fatalf("Failed to get second connection: %v", err)
}
connPool.Put(ctx, cn2)
}
// TestConnectionHealthCheckWithPushNotifications tests the isHealthyConn
// integration with push notifications.
func TestConnectionHealthCheckWithPushNotifications(t *testing.T) {
// Create client with push notifications
client := NewClient(&Options{
Addr: "localhost:6379",
Protocol: 3,
PushNotifications: true,
})
defer client.Close()
// Register a handler to ensure processor is active
err := client.RegisterPushNotificationHandler("TEST_HEALTH", newTestHandler(func(ctx context.Context, notification []interface{}) bool {
return true
}), false)
if err != nil {
t.Fatalf("Failed to register handler: %v", err)
}
// Test basic connection operations to exercise health checks
ctx := context.Background()
for i := 0; i < 5; i++ {
pong, err := client.Ping(ctx).Result()
if err != nil {
t.Fatalf("Ping failed: %v", err)
}
if pong != "PONG" {
t.Errorf("Expected PONG, got %s", pong)
}
}
}
// TestConnPushNotificationMethods tests all push notification methods on Conn type.
func TestConnPushNotificationMethods(t *testing.T) {
// Create client with push notifications
client := NewClient(&Options{
Addr: "localhost:6379",
Protocol: 3,
PushNotifications: true,
})
defer client.Close()
// Create a Conn instance
conn := client.Conn()
defer conn.Close()
// Test GetPushNotificationProcessor
processor := conn.GetPushNotificationProcessor()
if processor == nil {
t.Error("Conn should have push notification processor")
}
// Test that processor can handle handlers when enabled
testHandler := processor.GetHandler("TEST")
if testHandler != nil {
t.Error("Should not have handler for TEST initially")
}
// Test RegisterPushNotificationHandler
handler := newTestHandler(func(ctx context.Context, notification []interface{}) bool {
return true
})
err := conn.RegisterPushNotificationHandler("TEST_CONN_HANDLER", handler, false)
if err != nil {
t.Errorf("Failed to register handler on Conn: %v", err)
}
// Test RegisterPushNotificationHandler with function wrapper
err = conn.RegisterPushNotificationHandler("TEST_CONN_FUNC", newTestHandler(func(ctx context.Context, notification []interface{}) bool {
return true
}), false)
if err != nil {
t.Errorf("Failed to register handler func on Conn: %v", err)
}
// Test duplicate handler error
err = conn.RegisterPushNotificationHandler("TEST_CONN_HANDLER", handler, false)
if err == nil {
t.Error("Should get error when registering duplicate handler")
}
// Test that handlers work using GetHandler
ctx := context.Background()
connHandler := processor.GetHandler("TEST_CONN_HANDLER")
if connHandler == nil {
t.Error("Should have handler for TEST_CONN_HANDLER after registration")
return
}
handled := connHandler.HandlePushNotification(ctx, []interface{}{"TEST_CONN_HANDLER", "data"})
if !handled {
t.Error("Handler should have been called")
}
funcHandler := processor.GetHandler("TEST_CONN_FUNC")
if funcHandler == nil {
t.Error("Should have handler for TEST_CONN_FUNC after registration")
return
}
handled = funcHandler.HandlePushNotification(ctx, []interface{}{"TEST_CONN_FUNC", "data"})
if !handled {
t.Error("Handler func should have been called")
}
}
// TestConnWithoutPushNotifications tests Conn behavior when push notifications are disabled.
func TestConnWithoutPushNotifications(t *testing.T) {
// Create client without push notifications
client := NewClient(&Options{
Addr: "localhost:6379",
Protocol: 2, // RESP2, no push notifications
PushNotifications: false,
})
defer client.Close()
// Create a Conn instance
conn := client.Conn()
defer conn.Close()
// Test GetPushNotificationProcessor returns VoidPushNotificationProcessor
processor := conn.GetPushNotificationProcessor()
if processor == nil {
t.Error("Conn should always have a push notification processor")
}
// VoidPushNotificationProcessor should return nil for all handlers
handler := processor.GetHandler("TEST")
if handler != nil {
t.Error("VoidPushNotificationProcessor should return nil for all handlers")
}
// Test RegisterPushNotificationHandler returns nil (no error)
err := conn.RegisterPushNotificationHandler("TEST", newTestHandler(func(ctx context.Context, notification []interface{}) bool {
return true
}), false)
if err != nil {
t.Errorf("Should return nil error when no processor: %v", err)
}
// Test RegisterPushNotificationHandler returns nil (no error)
err = conn.RegisterPushNotificationHandler("TEST", newTestHandler(func(ctx context.Context, notification []interface{}) bool {
return true
}), false)
if err != nil {
t.Errorf("Should return nil error when no processor: %v", err)
}
}
// TestNewConnWithCustomProcessor tests newConn with custom processor in options.
func TestNewConnWithCustomProcessor(t *testing.T) {
// Create custom processor
customProcessor := NewPushNotificationProcessor()
// Create options with custom processor
opt := &Options{
Addr: "localhost:6379",
Protocol: 3,
PushNotificationProcessor: customProcessor,
}
opt.init()
// Create a mock connection pool
connPool := newConnPool(opt, func(ctx context.Context, network, addr string) (net.Conn, error) {
return nil, nil // Mock dialer
})
// Test that newConn sets the custom processor
conn := newConn(opt, connPool, nil)
if conn.GetPushNotificationProcessor() != customProcessor {
t.Error("newConn should set custom processor from options")
}
}
// TestClonedClientPushNotifications tests that cloned clients preserve push notifications.
func TestClonedClientPushNotifications(t *testing.T) {
// Create original client
client := NewClient(&Options{
Addr: "localhost:6379",
Protocol: 3,
})
defer client.Close()
originalProcessor := client.GetPushNotificationProcessor()
if originalProcessor == nil {
t.Fatal("Original client should have push notification processor")
}
// Register handler on original
err := client.RegisterPushNotificationHandler("TEST_CLONE", newTestHandler(func(ctx context.Context, notification []interface{}) bool {
return true
}), false)
if err != nil {
t.Fatalf("Failed to register handler: %v", err)
}
// Create cloned client with timeout
clonedClient := client.WithTimeout(5 * time.Second)
defer clonedClient.Close()
// Test that cloned client has same processor
clonedProcessor := clonedClient.GetPushNotificationProcessor()
if clonedProcessor != originalProcessor {
t.Error("Cloned client should have same push notification processor")
}
// Test that handlers work on cloned client using GetHandler
ctx := context.Background()
cloneHandler := clonedProcessor.GetHandler("TEST_CLONE")
if cloneHandler == nil {
t.Error("Cloned client should have TEST_CLONE handler")
return
}
handled := cloneHandler.HandlePushNotification(ctx, []interface{}{"TEST_CLONE", "data"})
if !handled {
t.Error("Cloned client should handle notifications")
}
// Test registering new handler on cloned client
err = clonedClient.RegisterPushNotificationHandler("TEST_CLONE_NEW", newTestHandler(func(ctx context.Context, notification []interface{}) bool {
return true
}), false)
if err != nil {
t.Errorf("Failed to register handler on cloned client: %v", err)
}
}
// TestPushNotificationInfoStructure tests the cleaned up PushNotificationInfo.
func TestPushNotificationInfoStructure(t *testing.T) {
// Test with various notification types
testCases := []struct {
name string
notification []interface{}
expectedCmd string
expectedArgs int
}{
{
name: "MOVING notification",
notification: []interface{}{"MOVING", "127.0.0.1:6380", "slot", "1234"},
expectedCmd: "MOVING",
expectedArgs: 3,
},
{
name: "MIGRATING notification",
notification: []interface{}{"MIGRATING", "time", "123456"},
expectedCmd: "MIGRATING",
expectedArgs: 2,
},
{
name: "MIGRATED notification",
notification: []interface{}{"MIGRATED"},
expectedCmd: "MIGRATED",
expectedArgs: 0,
},
{
name: "Custom notification",
notification: []interface{}{"CUSTOM_EVENT", "arg1", "arg2", "arg3"},
expectedCmd: "CUSTOM_EVENT",
expectedArgs: 3,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
info := ParsePushNotificationInfo(tc.notification)
if info.Name != tc.expectedCmd {
t.Errorf("Expected name %s, got %s", tc.expectedCmd, info.Name)
}
if len(info.Args) != tc.expectedArgs {
t.Errorf("Expected %d args, got %d", tc.expectedArgs, len(info.Args))
}
// Verify no unused fields exist by checking the struct only has Name and Args
// This is a compile-time check - if unused fields were added back, this would fail
_ = struct {
Name string
Args []interface{}
}{
Name: info.Name,
Args: info.Args,
}
})
}
}
// TestConnectionPoolOptionsIntegration tests that pool options correctly include processor.
func TestConnectionPoolOptionsIntegration(t *testing.T) {
// Create processor
processor := NewPushNotificationProcessor()
// Create options
opt := &Options{
Addr: "localhost:6379",
Protocol: 3,
PushNotificationProcessor: processor,
}
opt.init()
// Create connection pool
connPool := newConnPool(opt, func(ctx context.Context, network, addr string) (net.Conn, error) {
return nil, nil // Mock dialer
})
// Verify the pool has the processor in its configuration
// This tests the integration between options and pool creation
if connPool == nil {
t.Error("Connection pool should be created")
}
}
// TestProcessPendingNotificationsEdgeCases tests edge cases in ProcessPendingNotifications.
func TestProcessPendingNotificationsEdgeCases(t *testing.T) {
processor := NewPushNotificationProcessor()
ctx := context.Background()
// Test with nil reader (should not panic)
err := processor.ProcessPendingNotifications(ctx, nil)
if err != nil {
t.Logf("ProcessPendingNotifications correctly handles nil reader: %v", err)
}
// Test with empty reader
emptyReader := proto.NewReader(bytes.NewReader([]byte{}))
err = processor.ProcessPendingNotifications(ctx, emptyReader)
if err != nil {
t.Errorf("Should not error with empty reader: %v", err)
}
// Test with void processor (simulates disabled state)
voidProcessor := NewVoidPushNotificationProcessor()
err = voidProcessor.ProcessPendingNotifications(ctx, emptyReader)
if err != nil {
t.Errorf("Void processor should not error: %v", err)
}
}