mirror of
https://github.com/redis/go-redis.git
synced 2025-11-02 15:33:16 +03:00
* internal/proto/peek_push_notification_test : Refactor test helpers to use fmt.Fprintf for buffers Replaced buf.WriteString(fmt.Sprintf(...)) with fmt.Fprintf or fmt.Fprint in test helper functions for improved clarity and efficiency. This change affects push notification and RESP3 test utilities. * peek_push_notification_test: revert prev formatting * all: replace buf.WriteString with fmt.FprintF for consistency --------- Co-authored-by: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com>
1714 lines
50 KiB
Go
1714 lines
50 KiB
Go
package push
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/redis/go-redis/v9/internal/pool"
|
|
"github.com/redis/go-redis/v9/internal/proto"
|
|
)
|
|
|
|
// TestHandler implements NotificationHandler interface for testing
|
|
type TestHandler struct {
|
|
name string
|
|
handled [][]interface{}
|
|
returnError error
|
|
}
|
|
|
|
func NewTestHandler(name string) *TestHandler {
|
|
return &TestHandler{
|
|
name: name,
|
|
handled: make([][]interface{}, 0),
|
|
}
|
|
}
|
|
|
|
// MockNetConn implements net.Conn for testing
|
|
type MockNetConn struct{}
|
|
|
|
func (m *MockNetConn) Read(b []byte) (n int, err error) { return 0, nil }
|
|
func (m *MockNetConn) Write(b []byte) (n int, err error) { return len(b), nil }
|
|
func (m *MockNetConn) Close() error { return nil }
|
|
func (m *MockNetConn) LocalAddr() net.Addr { return nil }
|
|
func (m *MockNetConn) RemoteAddr() net.Addr { return nil }
|
|
func (m *MockNetConn) SetDeadline(t time.Time) error { return nil }
|
|
func (m *MockNetConn) SetReadDeadline(t time.Time) error { return nil }
|
|
func (m *MockNetConn) SetWriteDeadline(t time.Time) error { return nil }
|
|
|
|
func (h *TestHandler) HandlePushNotification(ctx context.Context, handlerCtx NotificationHandlerContext, notification []interface{}) error {
|
|
h.handled = append(h.handled, notification)
|
|
return h.returnError
|
|
}
|
|
|
|
func (h *TestHandler) GetHandledNotifications() [][]interface{} {
|
|
return h.handled
|
|
}
|
|
|
|
func (h *TestHandler) SetReturnError(err error) {
|
|
h.returnError = err
|
|
}
|
|
|
|
func (h *TestHandler) Reset() {
|
|
h.handled = make([][]interface{}, 0)
|
|
h.returnError = nil
|
|
}
|
|
|
|
// Mock client types for testing
|
|
type MockClient struct {
|
|
name string
|
|
}
|
|
|
|
type MockConnPool struct {
|
|
name string
|
|
}
|
|
|
|
type MockPubSub struct {
|
|
name string
|
|
}
|
|
|
|
// TestNotificationHandlerContext tests the handler context implementation
|
|
func TestNotificationHandlerContext(t *testing.T) {
|
|
t.Run("DirectObjectCreation", func(t *testing.T) {
|
|
client := &MockClient{name: "test-client"}
|
|
connPool := &MockConnPool{name: "test-pool"}
|
|
pubSub := &MockPubSub{name: "test-pubsub"}
|
|
conn := &pool.Conn{}
|
|
|
|
ctx := NotificationHandlerContext{
|
|
Client: client,
|
|
ConnPool: connPool,
|
|
PubSub: pubSub,
|
|
Conn: conn,
|
|
IsBlocking: true,
|
|
}
|
|
|
|
if ctx.Client != client {
|
|
t.Error("Client field should contain the provided client")
|
|
}
|
|
|
|
if ctx.ConnPool != connPool {
|
|
t.Error("ConnPool field should contain the provided connection pool")
|
|
}
|
|
|
|
if ctx.PubSub != pubSub {
|
|
t.Error("PubSub field should contain the provided PubSub")
|
|
}
|
|
|
|
if ctx.Conn != conn {
|
|
t.Error("Conn field should contain the provided connection")
|
|
}
|
|
|
|
if !ctx.IsBlocking {
|
|
t.Error("IsBlocking field should be true")
|
|
}
|
|
})
|
|
|
|
t.Run("NilValues", func(t *testing.T) {
|
|
ctx := NotificationHandlerContext{
|
|
Client: nil,
|
|
ConnPool: nil,
|
|
PubSub: nil,
|
|
Conn: nil,
|
|
IsBlocking: false,
|
|
}
|
|
|
|
if ctx.Client != nil {
|
|
t.Error("Client field should be nil when client is nil")
|
|
}
|
|
|
|
if ctx.ConnPool != nil {
|
|
t.Error("ConnPool field should be nil when connPool is nil")
|
|
}
|
|
|
|
if ctx.PubSub != nil {
|
|
t.Error("PubSub field should be nil when pubSub is nil")
|
|
}
|
|
|
|
if ctx.Conn != nil {
|
|
t.Error("Conn field should be nil when conn is nil")
|
|
}
|
|
|
|
if ctx.IsBlocking {
|
|
t.Error("IsBlocking field should be false")
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestRegistry tests the registry implementation
|
|
func TestRegistry(t *testing.T) {
|
|
t.Run("NewRegistry", func(t *testing.T) {
|
|
registry := NewRegistry()
|
|
if registry == nil {
|
|
t.Fatal("NewRegistry should not return nil")
|
|
}
|
|
|
|
if registry.handlers == nil {
|
|
t.Error("Registry handlers map should be initialized")
|
|
}
|
|
|
|
if registry.protected == nil {
|
|
t.Error("Registry protected map should be initialized")
|
|
}
|
|
})
|
|
|
|
t.Run("RegisterHandler", func(t *testing.T) {
|
|
registry := NewRegistry()
|
|
handler := NewTestHandler("test")
|
|
|
|
err := registry.RegisterHandler("TEST", handler, false)
|
|
if err != nil {
|
|
t.Errorf("RegisterHandler should not error: %v", err)
|
|
}
|
|
|
|
retrievedHandler := registry.GetHandler("TEST")
|
|
if retrievedHandler != handler {
|
|
t.Error("GetHandler should return the registered handler")
|
|
}
|
|
})
|
|
|
|
t.Run("RegisterNilHandler", func(t *testing.T) {
|
|
registry := NewRegistry()
|
|
|
|
err := registry.RegisterHandler("TEST", nil, false)
|
|
if err == nil {
|
|
t.Error("RegisterHandler should error when handler is nil")
|
|
}
|
|
|
|
if !strings.Contains(err.Error(), "handler cannot be nil") {
|
|
t.Errorf("Error message should mention nil handler, got: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("RegisterProtectedHandler", func(t *testing.T) {
|
|
registry := NewRegistry()
|
|
handler := NewTestHandler("test")
|
|
|
|
// Register protected handler
|
|
err := registry.RegisterHandler("TEST", handler, true)
|
|
if err != nil {
|
|
t.Errorf("RegisterHandler should not error: %v", err)
|
|
}
|
|
|
|
// Try to overwrite any existing handler (protected or not)
|
|
newHandler := NewTestHandler("new")
|
|
err = registry.RegisterHandler("TEST", newHandler, false)
|
|
if err == nil {
|
|
t.Error("RegisterHandler should error when trying to overwrite existing handler")
|
|
}
|
|
|
|
if !strings.Contains(err.Error(), "cannot overwrite existing handler") {
|
|
t.Errorf("Error message should mention existing handler, got: %v", err)
|
|
}
|
|
|
|
// Original handler should still be there
|
|
retrievedHandler := registry.GetHandler("TEST")
|
|
if retrievedHandler != handler {
|
|
t.Error("Existing handler should not be overwritten")
|
|
}
|
|
})
|
|
|
|
t.Run("CannotOverwriteExistingHandler", func(t *testing.T) {
|
|
registry := NewRegistry()
|
|
handler1 := NewTestHandler("test1")
|
|
handler2 := NewTestHandler("test2")
|
|
|
|
// Register non-protected handler
|
|
err := registry.RegisterHandler("TEST", handler1, false)
|
|
if err != nil {
|
|
t.Errorf("RegisterHandler should not error: %v", err)
|
|
}
|
|
|
|
// Try to overwrite with another handler (should fail)
|
|
err = registry.RegisterHandler("TEST", handler2, false)
|
|
if err == nil {
|
|
t.Error("RegisterHandler should error when trying to overwrite existing handler")
|
|
}
|
|
|
|
if !strings.Contains(err.Error(), "cannot overwrite existing handler") {
|
|
t.Errorf("Error message should mention existing handler, got: %v", err)
|
|
}
|
|
|
|
// Original handler should still be there
|
|
retrievedHandler := registry.GetHandler("TEST")
|
|
if retrievedHandler != handler1 {
|
|
t.Error("Existing handler should not be overwritten")
|
|
}
|
|
})
|
|
|
|
t.Run("GetNonExistentHandler", func(t *testing.T) {
|
|
registry := NewRegistry()
|
|
|
|
handler := registry.GetHandler("NONEXISTENT")
|
|
if handler != nil {
|
|
t.Error("GetHandler should return nil for non-existent handler")
|
|
}
|
|
})
|
|
|
|
t.Run("UnregisterHandler", func(t *testing.T) {
|
|
registry := NewRegistry()
|
|
handler := NewTestHandler("test")
|
|
|
|
registry.RegisterHandler("TEST", handler, false)
|
|
|
|
err := registry.UnregisterHandler("TEST")
|
|
if err != nil {
|
|
t.Errorf("UnregisterHandler should not error: %v", err)
|
|
}
|
|
|
|
retrievedHandler := registry.GetHandler("TEST")
|
|
if retrievedHandler != nil {
|
|
t.Error("GetHandler should return nil after unregistering")
|
|
}
|
|
})
|
|
|
|
t.Run("UnregisterProtectedHandler", func(t *testing.T) {
|
|
registry := NewRegistry()
|
|
handler := NewTestHandler("test")
|
|
|
|
// Register protected handler
|
|
registry.RegisterHandler("TEST", handler, true)
|
|
|
|
// Try to unregister protected handler
|
|
err := registry.UnregisterHandler("TEST")
|
|
if err == nil {
|
|
t.Error("UnregisterHandler should error for protected handler")
|
|
}
|
|
|
|
if !strings.Contains(err.Error(), "handler is protected") {
|
|
t.Errorf("Error message should mention handler is protected, got: %v", err)
|
|
}
|
|
|
|
// Handler should still be there
|
|
retrievedHandler := registry.GetHandler("TEST")
|
|
if retrievedHandler != handler {
|
|
t.Error("Protected handler should still be registered")
|
|
}
|
|
})
|
|
|
|
t.Run("UnregisterNonExistentHandler", func(t *testing.T) {
|
|
registry := NewRegistry()
|
|
|
|
err := registry.UnregisterHandler("NONEXISTENT")
|
|
if err != nil {
|
|
t.Errorf("UnregisterHandler should not error for non-existent handler: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("CannotOverwriteExistingHandler", func(t *testing.T) {
|
|
registry := NewRegistry()
|
|
handler1 := NewTestHandler("handler1")
|
|
handler2 := NewTestHandler("handler2")
|
|
|
|
// Register first handler (non-protected)
|
|
err := registry.RegisterHandler("TEST_NOTIFICATION", handler1, false)
|
|
if err != nil {
|
|
t.Errorf("First RegisterHandler should not error: %v", err)
|
|
}
|
|
|
|
// Verify first handler is registered
|
|
retrievedHandler := registry.GetHandler("TEST_NOTIFICATION")
|
|
if retrievedHandler != handler1 {
|
|
t.Error("First handler should be registered correctly")
|
|
}
|
|
|
|
// Attempt to overwrite with second handler (should fail)
|
|
err = registry.RegisterHandler("TEST_NOTIFICATION", handler2, false)
|
|
if err == nil {
|
|
t.Error("RegisterHandler should error when trying to overwrite existing handler")
|
|
}
|
|
|
|
// Verify error message mentions overwriting
|
|
if !strings.Contains(err.Error(), "cannot overwrite existing handler") {
|
|
t.Errorf("Error message should mention overwriting existing handler, got: %v", err)
|
|
}
|
|
|
|
// Verify error message includes the notification name
|
|
if !strings.Contains(err.Error(), "TEST_NOTIFICATION") {
|
|
t.Errorf("Error message should include notification name, got: %v", err)
|
|
}
|
|
|
|
// Verify original handler is still there (not overwritten)
|
|
retrievedHandler = registry.GetHandler("TEST_NOTIFICATION")
|
|
if retrievedHandler != handler1 {
|
|
t.Error("Original handler should still be registered (not overwritten)")
|
|
}
|
|
|
|
// Verify second handler was NOT registered
|
|
if retrievedHandler == handler2 {
|
|
t.Error("Second handler should NOT be registered")
|
|
}
|
|
})
|
|
|
|
t.Run("CannotOverwriteProtectedHandler", func(t *testing.T) {
|
|
registry := NewRegistry()
|
|
protectedHandler := NewTestHandler("protected")
|
|
newHandler := NewTestHandler("new")
|
|
|
|
// Register protected handler
|
|
err := registry.RegisterHandler("PROTECTED_NOTIFICATION", protectedHandler, true)
|
|
if err != nil {
|
|
t.Errorf("RegisterHandler should not error for protected handler: %v", err)
|
|
}
|
|
|
|
// Attempt to overwrite protected handler (should fail)
|
|
err = registry.RegisterHandler("PROTECTED_NOTIFICATION", newHandler, false)
|
|
if err == nil {
|
|
t.Error("RegisterHandler should error when trying to overwrite protected handler")
|
|
}
|
|
|
|
// Verify error message
|
|
if !strings.Contains(err.Error(), "cannot overwrite existing handler") {
|
|
t.Errorf("Error message should mention overwriting existing handler, got: %v", err)
|
|
}
|
|
|
|
// Verify protected handler is still there
|
|
retrievedHandler := registry.GetHandler("PROTECTED_NOTIFICATION")
|
|
if retrievedHandler != protectedHandler {
|
|
t.Error("Protected handler should still be registered")
|
|
}
|
|
})
|
|
|
|
t.Run("CanRegisterDifferentHandlers", func(t *testing.T) {
|
|
registry := NewRegistry()
|
|
handler1 := NewTestHandler("handler1")
|
|
handler2 := NewTestHandler("handler2")
|
|
|
|
// Register handlers for different notification names (should succeed)
|
|
err := registry.RegisterHandler("NOTIFICATION_1", handler1, false)
|
|
if err != nil {
|
|
t.Errorf("RegisterHandler should not error for first notification: %v", err)
|
|
}
|
|
|
|
err = registry.RegisterHandler("NOTIFICATION_2", handler2, true)
|
|
if err != nil {
|
|
t.Errorf("RegisterHandler should not error for second notification: %v", err)
|
|
}
|
|
|
|
// Verify both handlers are registered correctly
|
|
retrievedHandler1 := registry.GetHandler("NOTIFICATION_1")
|
|
if retrievedHandler1 != handler1 {
|
|
t.Error("First handler should be registered correctly")
|
|
}
|
|
|
|
retrievedHandler2 := registry.GetHandler("NOTIFICATION_2")
|
|
if retrievedHandler2 != handler2 {
|
|
t.Error("Second handler should be registered correctly")
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestProcessor tests the processor implementation
|
|
func TestProcessor(t *testing.T) {
|
|
t.Run("NewProcessor", func(t *testing.T) {
|
|
processor := NewProcessor()
|
|
if processor == nil {
|
|
t.Fatal("NewProcessor should not return nil")
|
|
}
|
|
|
|
if processor.registry == nil {
|
|
t.Error("Processor should have a registry")
|
|
}
|
|
})
|
|
|
|
t.Run("RegisterAndGetHandler", func(t *testing.T) {
|
|
processor := NewProcessor()
|
|
handler := NewTestHandler("test")
|
|
|
|
err := processor.RegisterHandler("TEST", handler, false)
|
|
if err != nil {
|
|
t.Errorf("RegisterHandler should not error: %v", err)
|
|
}
|
|
|
|
retrievedHandler := processor.GetHandler("TEST")
|
|
if retrievedHandler != handler {
|
|
t.Error("GetHandler should return the registered handler")
|
|
}
|
|
})
|
|
|
|
t.Run("UnregisterHandler", func(t *testing.T) {
|
|
processor := NewProcessor()
|
|
handler := NewTestHandler("test")
|
|
|
|
processor.RegisterHandler("TEST", handler, false)
|
|
|
|
err := processor.UnregisterHandler("TEST")
|
|
if err != nil {
|
|
t.Errorf("UnregisterHandler should not error: %v", err)
|
|
}
|
|
|
|
retrievedHandler := processor.GetHandler("TEST")
|
|
if retrievedHandler != nil {
|
|
t.Error("GetHandler should return nil after unregistering")
|
|
}
|
|
})
|
|
|
|
t.Run("ProcessPendingNotifications_NilReader", func(t *testing.T) {
|
|
processor := NewProcessor()
|
|
ctx := context.Background()
|
|
handlerCtx := NotificationHandlerContext{
|
|
Client: nil,
|
|
ConnPool: nil,
|
|
PubSub: nil,
|
|
Conn: nil,
|
|
IsBlocking: false,
|
|
}
|
|
|
|
err := processor.ProcessPendingNotifications(ctx, handlerCtx, nil)
|
|
if err != nil {
|
|
t.Errorf("ProcessPendingNotifications should not error with nil reader: %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestVoidProcessor tests the void processor implementation
|
|
func TestVoidProcessor(t *testing.T) {
|
|
t.Run("NewVoidProcessor", func(t *testing.T) {
|
|
processor := NewVoidProcessor()
|
|
if processor == nil {
|
|
t.Error("NewVoidProcessor should not return nil")
|
|
}
|
|
})
|
|
|
|
t.Run("GetHandler", func(t *testing.T) {
|
|
processor := NewVoidProcessor()
|
|
handler := processor.GetHandler("TEST")
|
|
if handler != nil {
|
|
t.Error("VoidProcessor GetHandler should always return nil")
|
|
}
|
|
})
|
|
|
|
t.Run("RegisterHandler", func(t *testing.T) {
|
|
processor := NewVoidProcessor()
|
|
handler := NewTestHandler("test")
|
|
|
|
err := processor.RegisterHandler("TEST", handler, false)
|
|
if err == nil {
|
|
t.Error("VoidProcessor RegisterHandler should return error")
|
|
}
|
|
|
|
if !strings.Contains(err.Error(), "register failed") {
|
|
t.Errorf("Error message should mention registration failure, got: %v", err)
|
|
}
|
|
|
|
if !strings.Contains(err.Error(), "push notifications are disabled") {
|
|
t.Errorf("Error message should mention disabled notifications, got: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("UnregisterHandler", func(t *testing.T) {
|
|
processor := NewVoidProcessor()
|
|
|
|
err := processor.UnregisterHandler("TEST")
|
|
if err == nil {
|
|
t.Error("VoidProcessor UnregisterHandler should return error")
|
|
}
|
|
|
|
if !strings.Contains(err.Error(), "unregister failed") {
|
|
t.Errorf("Error message should mention unregistration failure, got: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("ProcessPendingNotifications_NilReader", func(t *testing.T) {
|
|
processor := NewVoidProcessor()
|
|
ctx := context.Background()
|
|
handlerCtx := NotificationHandlerContext{
|
|
Client: nil,
|
|
ConnPool: nil,
|
|
PubSub: nil,
|
|
Conn: nil,
|
|
IsBlocking: false,
|
|
}
|
|
|
|
err := processor.ProcessPendingNotifications(ctx, handlerCtx, nil)
|
|
if err != nil {
|
|
t.Errorf("VoidProcessor ProcessPendingNotifications should never error, got: %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestShouldSkipNotification tests the notification filtering logic
|
|
func TestShouldSkipNotification(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
notification string
|
|
shouldSkip bool
|
|
}{
|
|
// Pub/Sub notifications that should be skipped
|
|
{"message", "message", true},
|
|
{"pmessage", "pmessage", true},
|
|
{"subscribe", "subscribe", true},
|
|
{"unsubscribe", "unsubscribe", true},
|
|
{"psubscribe", "psubscribe", true},
|
|
{"punsubscribe", "punsubscribe", true},
|
|
{"smessage", "smessage", true},
|
|
{"ssubscribe", "ssubscribe", true},
|
|
{"sunsubscribe", "sunsubscribe", true},
|
|
|
|
// Push notifications that should NOT be skipped
|
|
{"MOVING", "MOVING", false},
|
|
{"MIGRATING", "MIGRATING", false},
|
|
{"MIGRATED", "MIGRATED", false},
|
|
{"FAILING_OVER", "FAILING_OVER", false},
|
|
{"FAILED_OVER", "FAILED_OVER", false},
|
|
{"custom", "custom", false},
|
|
{"unknown", "unknown", false},
|
|
{"empty", "", false},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
result := willHandleNotificationInClient(tc.notification)
|
|
if result != tc.shouldSkip {
|
|
t.Errorf("willHandleNotificationInClient(%q) = %v, want %v", tc.notification, result, tc.shouldSkip)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestNotificationHandlerInterface tests that our test handler implements the interface correctly
|
|
func TestNotificationHandlerInterface(t *testing.T) {
|
|
var _ NotificationHandler = (*TestHandler)(nil)
|
|
|
|
handler := NewTestHandler("test")
|
|
ctx := context.Background()
|
|
handlerCtx := NotificationHandlerContext{
|
|
Client: nil,
|
|
ConnPool: nil,
|
|
PubSub: nil,
|
|
Conn: nil,
|
|
IsBlocking: false,
|
|
}
|
|
notification := []interface{}{"TEST", "data"}
|
|
|
|
err := handler.HandlePushNotification(ctx, handlerCtx, notification)
|
|
if err != nil {
|
|
t.Errorf("HandlePushNotification should not error: %v", err)
|
|
}
|
|
|
|
handled := handler.GetHandledNotifications()
|
|
if len(handled) != 1 {
|
|
t.Errorf("Expected 1 handled notification, got %d", len(handled))
|
|
}
|
|
|
|
if len(handled[0]) != 2 || handled[0][0] != "TEST" || handled[0][1] != "data" {
|
|
t.Errorf("Handled notification should match input: %v", handled[0])
|
|
}
|
|
}
|
|
|
|
// TestNotificationHandlerError tests error handling in handlers
|
|
func TestNotificationHandlerError(t *testing.T) {
|
|
handler := NewTestHandler("test")
|
|
expectedError := errors.New("test error")
|
|
handler.SetReturnError(expectedError)
|
|
|
|
ctx := context.Background()
|
|
handlerCtx := NotificationHandlerContext{
|
|
Client: nil,
|
|
ConnPool: nil,
|
|
PubSub: nil,
|
|
Conn: nil,
|
|
IsBlocking: false,
|
|
}
|
|
notification := []interface{}{"TEST", "data"}
|
|
|
|
err := handler.HandlePushNotification(ctx, handlerCtx, notification)
|
|
if err != expectedError {
|
|
t.Errorf("HandlePushNotification should return the set error: got %v, want %v", err, expectedError)
|
|
}
|
|
|
|
// Reset and test no error
|
|
handler.Reset()
|
|
err = handler.HandlePushNotification(ctx, handlerCtx, notification)
|
|
if err != nil {
|
|
t.Errorf("HandlePushNotification should not error after reset: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestRegistryConcurrency tests concurrent access to registry
|
|
func TestRegistryConcurrency(t *testing.T) {
|
|
registry := NewRegistry()
|
|
|
|
// Test concurrent registration and access
|
|
done := make(chan bool, 10)
|
|
|
|
// Start multiple goroutines registering handlers
|
|
for i := 0; i < 5; i++ {
|
|
go func(id int) {
|
|
handler := NewTestHandler("test")
|
|
err := registry.RegisterHandler(fmt.Sprintf("TEST_%d", id), handler, false)
|
|
if err != nil {
|
|
t.Errorf("RegisterHandler should not error: %v", err)
|
|
}
|
|
done <- true
|
|
}(i)
|
|
}
|
|
|
|
// Start multiple goroutines reading handlers
|
|
for i := 0; i < 5; i++ {
|
|
go func(id int) {
|
|
registry.GetHandler(fmt.Sprintf("TEST_%d", id))
|
|
done <- true
|
|
}(i)
|
|
}
|
|
|
|
// Wait for all goroutines to complete
|
|
for i := 0; i < 10; i++ {
|
|
<-done
|
|
}
|
|
}
|
|
|
|
// TestProcessorConcurrency tests concurrent access to processor
|
|
func TestProcessorConcurrency(t *testing.T) {
|
|
processor := NewProcessor()
|
|
|
|
// Test concurrent registration and access
|
|
done := make(chan bool, 10)
|
|
|
|
// Start multiple goroutines registering handlers
|
|
for i := 0; i < 5; i++ {
|
|
go func(id int) {
|
|
handler := NewTestHandler("test")
|
|
err := processor.RegisterHandler(fmt.Sprintf("TEST_%d", id), handler, false)
|
|
if err != nil {
|
|
t.Errorf("RegisterHandler should not error: %v", err)
|
|
}
|
|
done <- true
|
|
}(i)
|
|
}
|
|
|
|
// Start multiple goroutines reading handlers
|
|
for i := 0; i < 5; i++ {
|
|
go func(id int) {
|
|
processor.GetHandler(fmt.Sprintf("TEST_%d", id))
|
|
done <- true
|
|
}(i)
|
|
}
|
|
|
|
// Wait for all goroutines to complete
|
|
for i := 0; i < 10; i++ {
|
|
<-done
|
|
}
|
|
}
|
|
|
|
// TestRegistryEdgeCases tests edge cases for registry
|
|
func TestRegistryEdgeCases(t *testing.T) {
|
|
t.Run("RegisterHandlerWithEmptyName", func(t *testing.T) {
|
|
registry := NewRegistry()
|
|
handler := NewTestHandler("test")
|
|
|
|
err := registry.RegisterHandler("", handler, false)
|
|
if err != nil {
|
|
t.Errorf("RegisterHandler should not error with empty name: %v", err)
|
|
}
|
|
|
|
retrievedHandler := registry.GetHandler("")
|
|
if retrievedHandler != handler {
|
|
t.Error("GetHandler should return handler even with empty name")
|
|
}
|
|
})
|
|
|
|
t.Run("MultipleProtectedHandlers", func(t *testing.T) {
|
|
registry := NewRegistry()
|
|
handler1 := NewTestHandler("test1")
|
|
handler2 := NewTestHandler("test2")
|
|
|
|
// Register multiple protected handlers
|
|
err := registry.RegisterHandler("TEST1", handler1, true)
|
|
if err != nil {
|
|
t.Errorf("RegisterHandler should not error: %v", err)
|
|
}
|
|
|
|
err = registry.RegisterHandler("TEST2", handler2, true)
|
|
if err != nil {
|
|
t.Errorf("RegisterHandler should not error: %v", err)
|
|
}
|
|
|
|
// Try to unregister both
|
|
err = registry.UnregisterHandler("TEST1")
|
|
if err == nil {
|
|
t.Error("UnregisterHandler should error for protected handler")
|
|
}
|
|
|
|
err = registry.UnregisterHandler("TEST2")
|
|
if err == nil {
|
|
t.Error("UnregisterHandler should error for protected handler")
|
|
}
|
|
})
|
|
|
|
t.Run("CannotOverwriteAnyExistingHandler", func(t *testing.T) {
|
|
registry := NewRegistry()
|
|
handler1 := NewTestHandler("test1")
|
|
handler2 := NewTestHandler("test2")
|
|
|
|
// Register protected handler
|
|
err := registry.RegisterHandler("TEST", handler1, true)
|
|
if err != nil {
|
|
t.Errorf("RegisterHandler should not error: %v", err)
|
|
}
|
|
|
|
// Try to overwrite with another protected handler (should fail)
|
|
err = registry.RegisterHandler("TEST", handler2, true)
|
|
if err == nil {
|
|
t.Error("RegisterHandler should error when trying to overwrite existing handler")
|
|
}
|
|
|
|
if !strings.Contains(err.Error(), "cannot overwrite existing handler") {
|
|
t.Errorf("Error message should mention existing handler, got: %v", err)
|
|
}
|
|
|
|
// Original handler should still be there
|
|
retrievedHandler := registry.GetHandler("TEST")
|
|
if retrievedHandler != handler1 {
|
|
t.Error("Existing handler should not be overwritten")
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestProcessorEdgeCases tests edge cases for processor
|
|
func TestProcessorEdgeCases(t *testing.T) {
|
|
t.Run("ProcessorWithNilRegistry", func(t *testing.T) {
|
|
// This tests internal consistency - processor should always have a registry
|
|
processor := &Processor{registry: nil}
|
|
|
|
// This should panic or handle gracefully
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
// Expected behavior - accessing nil registry should panic
|
|
t.Logf("Expected panic when accessing nil registry: %v", r)
|
|
}
|
|
}()
|
|
|
|
// This will likely panic, which is expected behavior
|
|
processor.GetHandler("TEST")
|
|
})
|
|
|
|
t.Run("ProcessorRegisterNilHandler", func(t *testing.T) {
|
|
processor := NewProcessor()
|
|
|
|
err := processor.RegisterHandler("TEST", nil, false)
|
|
if err == nil {
|
|
t.Error("RegisterHandler should error when handler is nil")
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestVoidProcessorEdgeCases tests edge cases for void processor
|
|
func TestVoidProcessorEdgeCases(t *testing.T) {
|
|
t.Run("VoidProcessorMultipleOperations", func(t *testing.T) {
|
|
processor := NewVoidProcessor()
|
|
handler := NewTestHandler("test")
|
|
|
|
// Multiple register attempts should all fail
|
|
for i := 0; i < 5; i++ {
|
|
err := processor.RegisterHandler(fmt.Sprintf("TEST_%d", i), handler, false)
|
|
if err == nil {
|
|
t.Errorf("VoidProcessor RegisterHandler should always return error")
|
|
}
|
|
}
|
|
|
|
// Multiple unregister attempts should all fail
|
|
for i := 0; i < 5; i++ {
|
|
err := processor.UnregisterHandler(fmt.Sprintf("TEST_%d", i))
|
|
if err == nil {
|
|
t.Errorf("VoidProcessor UnregisterHandler should always return error")
|
|
}
|
|
}
|
|
|
|
// Multiple get attempts should all return nil
|
|
for i := 0; i < 5; i++ {
|
|
handler := processor.GetHandler(fmt.Sprintf("TEST_%d", i))
|
|
if handler != nil {
|
|
t.Errorf("VoidProcessor GetHandler should always return nil")
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// Helper functions to create fake RESP3 protocol data for testing
|
|
|
|
// createFakeRESP3PushNotification creates a fake RESP3 push notification buffer
|
|
func createFakeRESP3PushNotification(notificationType string, args ...string) *bytes.Buffer {
|
|
buf := &bytes.Buffer{}
|
|
|
|
// RESP3 Push notification format: ><len>\r\n<elements>\r\n
|
|
totalElements := 1 + len(args) // notification type + arguments
|
|
fmt.Fprintf(buf, ">%d\r\n", totalElements)
|
|
|
|
// Write notification type as bulk string
|
|
fmt.Fprintf(buf, "$%d\r\n%s\r\n", len(notificationType), notificationType)
|
|
|
|
// Write arguments as bulk strings
|
|
for _, arg := range args {
|
|
fmt.Fprintf(buf, "$%d\r\n%s\r\n", len(arg), arg)
|
|
}
|
|
|
|
return buf
|
|
}
|
|
|
|
// createReaderWithPrimedBuffer creates a reader (no longer needs priming)
|
|
func createReaderWithPrimedBuffer(buf *bytes.Buffer) *proto.Reader {
|
|
reader := proto.NewReader(buf)
|
|
// No longer need to prime the buffer - PeekPushNotificationName handles it automatically
|
|
return reader
|
|
}
|
|
|
|
// createMockConnection creates a mock connection for testing
|
|
func createMockConnection() *pool.Conn {
|
|
mockNetConn := &MockNetConn{}
|
|
return pool.NewConn(mockNetConn)
|
|
}
|
|
|
|
// createFakeRESP3Array creates a fake RESP3 array (not push notification)
|
|
func createFakeRESP3Array(elements ...string) *bytes.Buffer {
|
|
buf := &bytes.Buffer{}
|
|
|
|
// RESP3 Array format: *<len>\r\n<elements>\r\n
|
|
fmt.Fprintf(buf, "*%d\r\n", len(elements))
|
|
|
|
// Write elements as bulk strings
|
|
for _, element := range elements {
|
|
fmt.Fprintf(buf, "$%d\r\n%s\r\n", len(element), element)
|
|
}
|
|
|
|
return buf
|
|
}
|
|
|
|
// createFakeRESP3Error creates a fake RESP3 error
|
|
func createFakeRESP3Error(message string) *bytes.Buffer {
|
|
buf := &bytes.Buffer{}
|
|
fmt.Fprintf(buf, "-%s\r\n", message)
|
|
return buf
|
|
}
|
|
|
|
// createMultipleNotifications creates a buffer with multiple notifications
|
|
func createMultipleNotifications(notifications ...[]string) *bytes.Buffer {
|
|
buf := &bytes.Buffer{}
|
|
|
|
for _, notification := range notifications {
|
|
if len(notification) == 0 {
|
|
continue
|
|
}
|
|
|
|
notificationType := notification[0]
|
|
args := notification[1:]
|
|
|
|
// Determine if this should be a push notification or regular array
|
|
if willHandleNotificationInClient(notificationType) {
|
|
// Create as push notification (will be skipped)
|
|
pushBuf := createFakeRESP3PushNotification(notificationType, args...)
|
|
buf.Write(pushBuf.Bytes())
|
|
} else {
|
|
// Create as push notification (will be processed)
|
|
pushBuf := createFakeRESP3PushNotification(notificationType, args...)
|
|
buf.Write(pushBuf.Bytes())
|
|
}
|
|
}
|
|
|
|
return buf
|
|
}
|
|
|
|
// TestProcessorWithFakeBuffer tests ProcessPendingNotifications with fake RESP3 data
|
|
func TestProcessorWithFakeBuffer(t *testing.T) {
|
|
t.Run("ProcessValidPushNotification", func(t *testing.T) {
|
|
processor := NewProcessor()
|
|
handler := NewTestHandler("test")
|
|
processor.RegisterHandler("MOVING", handler, false)
|
|
|
|
// Create fake RESP3 push notification
|
|
buf := createFakeRESP3PushNotification("MOVING", "slot", "123", "from", "node1", "to", "node2")
|
|
reader := createReaderWithPrimedBuffer(buf)
|
|
|
|
ctx := context.Background()
|
|
handlerCtx := NotificationHandlerContext{
|
|
Client: nil,
|
|
ConnPool: nil,
|
|
PubSub: nil,
|
|
Conn: createMockConnection(),
|
|
IsBlocking: false,
|
|
}
|
|
|
|
err := processor.ProcessPendingNotifications(ctx, handlerCtx, reader)
|
|
if err != nil {
|
|
t.Errorf("ProcessPendingNotifications should not error: %v", err)
|
|
}
|
|
|
|
handled := handler.GetHandledNotifications()
|
|
if len(handled) != 1 {
|
|
t.Errorf("Expected 1 handled notification, got %d", len(handled))
|
|
return // Prevent panic if no notifications were handled
|
|
}
|
|
|
|
if len(handled[0]) != 7 || handled[0][0] != "MOVING" {
|
|
t.Errorf("Handled notification should match input: %v", handled[0])
|
|
}
|
|
|
|
if len(handled[0]) > 2 && (handled[0][1] != "slot" || handled[0][2] != "123") {
|
|
t.Errorf("Notification arguments should match: %v", handled[0])
|
|
}
|
|
})
|
|
|
|
t.Run("ProcessSkippedPushNotification", func(t *testing.T) {
|
|
processor := NewProcessor()
|
|
handler := NewTestHandler("test")
|
|
processor.RegisterHandler("message", handler, false)
|
|
|
|
// Create fake RESP3 push notification for pub/sub message (should be skipped)
|
|
buf := createFakeRESP3PushNotification("message", "channel", "hello world")
|
|
reader := createReaderWithPrimedBuffer(buf)
|
|
|
|
ctx := context.Background()
|
|
handlerCtx := NotificationHandlerContext{
|
|
Client: nil,
|
|
ConnPool: nil,
|
|
PubSub: nil,
|
|
Conn: createMockConnection(),
|
|
IsBlocking: false,
|
|
}
|
|
|
|
err := processor.ProcessPendingNotifications(ctx, handlerCtx, reader)
|
|
if err != nil {
|
|
t.Errorf("ProcessPendingNotifications should not error: %v", err)
|
|
}
|
|
|
|
handled := handler.GetHandledNotifications()
|
|
if len(handled) != 0 {
|
|
t.Errorf("Expected 0 handled notifications (should be skipped), got %d", len(handled))
|
|
}
|
|
})
|
|
|
|
t.Run("ProcessNotificationWithoutHandler", func(t *testing.T) {
|
|
processor := NewProcessor()
|
|
// No handler registered for MOVING
|
|
|
|
// Create fake RESP3 push notification
|
|
buf := createFakeRESP3PushNotification("MOVING", "slot", "123")
|
|
reader := createReaderWithPrimedBuffer(buf)
|
|
|
|
ctx := context.Background()
|
|
handlerCtx := NotificationHandlerContext{
|
|
Client: nil,
|
|
ConnPool: nil,
|
|
PubSub: nil,
|
|
Conn: createMockConnection(),
|
|
IsBlocking: false,
|
|
}
|
|
|
|
err := processor.ProcessPendingNotifications(ctx, handlerCtx, reader)
|
|
if err != nil {
|
|
t.Errorf("ProcessPendingNotifications should not error when no handler: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("ProcessNotificationWithHandlerError", func(t *testing.T) {
|
|
processor := NewProcessor()
|
|
handler := NewTestHandler("test")
|
|
handler.SetReturnError(errors.New("handler error"))
|
|
processor.RegisterHandler("MOVING", handler, false)
|
|
|
|
// Create fake RESP3 push notification
|
|
buf := createFakeRESP3PushNotification("MOVING", "slot", "123")
|
|
reader := createReaderWithPrimedBuffer(buf)
|
|
|
|
ctx := context.Background()
|
|
handlerCtx := NotificationHandlerContext{
|
|
Client: nil,
|
|
ConnPool: nil,
|
|
PubSub: nil,
|
|
Conn: createMockConnection(),
|
|
IsBlocking: false,
|
|
}
|
|
|
|
err := processor.ProcessPendingNotifications(ctx, handlerCtx, reader)
|
|
if err != nil {
|
|
t.Errorf("ProcessPendingNotifications should not error even when handler errors: %v", err)
|
|
}
|
|
|
|
handled := handler.GetHandledNotifications()
|
|
if len(handled) != 1 {
|
|
t.Errorf("Expected 1 handled notification even with error, got %d", len(handled))
|
|
}
|
|
})
|
|
|
|
t.Run("ProcessNonPushNotification", func(t *testing.T) {
|
|
processor := NewProcessor()
|
|
handler := NewTestHandler("test")
|
|
processor.RegisterHandler("MOVING", handler, false)
|
|
|
|
// Create fake RESP3 array (not push notification)
|
|
buf := createFakeRESP3Array("MOVING", "slot", "123")
|
|
reader := createReaderWithPrimedBuffer(buf)
|
|
|
|
ctx := context.Background()
|
|
handlerCtx := NotificationHandlerContext{
|
|
Client: nil,
|
|
ConnPool: nil,
|
|
PubSub: nil,
|
|
Conn: createMockConnection(),
|
|
IsBlocking: false,
|
|
}
|
|
|
|
err := processor.ProcessPendingNotifications(ctx, handlerCtx, reader)
|
|
if err != nil {
|
|
t.Errorf("ProcessPendingNotifications should not error: %v", err)
|
|
}
|
|
|
|
handled := handler.GetHandledNotifications()
|
|
if len(handled) != 0 {
|
|
t.Errorf("Expected 0 handled notifications (not push type), got %d", len(handled))
|
|
}
|
|
})
|
|
|
|
t.Run("ProcessMultipleNotifications", func(t *testing.T) {
|
|
processor := NewProcessor()
|
|
movingHandler := NewTestHandler("moving")
|
|
migratingHandler := NewTestHandler("migrating")
|
|
processor.RegisterHandler("MOVING", movingHandler, false)
|
|
processor.RegisterHandler("MIGRATING", migratingHandler, false)
|
|
|
|
// Create buffer with multiple notifications
|
|
buf := createMultipleNotifications(
|
|
[]string{"MOVING", "slot", "123", "from", "node1", "to", "node2"},
|
|
[]string{"MIGRATING", "slot", "456", "from", "node2", "to", "node3"},
|
|
)
|
|
reader := createReaderWithPrimedBuffer(buf)
|
|
|
|
ctx := context.Background()
|
|
handlerCtx := NotificationHandlerContext{
|
|
Client: nil,
|
|
ConnPool: nil,
|
|
PubSub: nil,
|
|
Conn: createMockConnection(),
|
|
IsBlocking: false,
|
|
}
|
|
|
|
err := processor.ProcessPendingNotifications(ctx, handlerCtx, reader)
|
|
if err != nil {
|
|
t.Errorf("ProcessPendingNotifications should not error: %v", err)
|
|
}
|
|
|
|
// Check MOVING handler
|
|
movingHandled := movingHandler.GetHandledNotifications()
|
|
if len(movingHandled) != 1 {
|
|
t.Errorf("Expected 1 MOVING notification, got %d", len(movingHandled))
|
|
}
|
|
if len(movingHandled) > 0 && movingHandled[0][0] != "MOVING" {
|
|
t.Errorf("Expected MOVING notification, got %v", movingHandled[0][0])
|
|
}
|
|
|
|
// Check MIGRATING handler
|
|
migratingHandled := migratingHandler.GetHandledNotifications()
|
|
if len(migratingHandled) != 1 {
|
|
t.Errorf("Expected 1 MIGRATING notification, got %d", len(migratingHandled))
|
|
}
|
|
if len(migratingHandled) > 0 && migratingHandled[0][0] != "MIGRATING" {
|
|
t.Errorf("Expected MIGRATING notification, got %v", migratingHandled[0][0])
|
|
}
|
|
})
|
|
|
|
t.Run("ProcessEmptyNotification", func(t *testing.T) {
|
|
processor := NewProcessor()
|
|
handler := NewTestHandler("test")
|
|
processor.RegisterHandler("MOVING", handler, false)
|
|
|
|
// Create fake RESP3 push notification with no elements
|
|
buf := &bytes.Buffer{}
|
|
fmt.Fprint(buf, ">0\r\n") // Empty push notification
|
|
reader := createReaderWithPrimedBuffer(buf)
|
|
|
|
ctx := context.Background()
|
|
handlerCtx := NotificationHandlerContext{
|
|
Client: nil,
|
|
ConnPool: nil,
|
|
PubSub: nil,
|
|
Conn: createMockConnection(),
|
|
IsBlocking: false,
|
|
}
|
|
|
|
// This should panic due to empty notification array
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
t.Logf("ProcessPendingNotifications panicked as expected for empty notification: %v", r)
|
|
}
|
|
}()
|
|
|
|
err := processor.ProcessPendingNotifications(ctx, handlerCtx, reader)
|
|
if err != nil {
|
|
t.Logf("ProcessPendingNotifications errored for empty notification: %v", err)
|
|
}
|
|
|
|
handled := handler.GetHandledNotifications()
|
|
if len(handled) != 0 {
|
|
t.Errorf("Expected 0 handled notifications for empty notification, got %d", len(handled))
|
|
}
|
|
})
|
|
|
|
t.Run("ProcessNotificationWithNonStringType", func(t *testing.T) {
|
|
processor := NewProcessor()
|
|
handler := NewTestHandler("test")
|
|
processor.RegisterHandler("MOVING", handler, false)
|
|
|
|
// Create fake RESP3 push notification with integer as first element
|
|
buf := &bytes.Buffer{}
|
|
fmt.Fprint(buf, ">2\r\n") // 2 elements
|
|
fmt.Fprint(buf, ":123\r\n") // Integer instead of string
|
|
fmt.Fprint(buf, "$4\r\ndata\r\n") // String data
|
|
reader := proto.NewReader(buf)
|
|
|
|
ctx := context.Background()
|
|
handlerCtx := NotificationHandlerContext{
|
|
Client: nil,
|
|
ConnPool: nil,
|
|
PubSub: nil,
|
|
Conn: createMockConnection(),
|
|
IsBlocking: false,
|
|
}
|
|
|
|
err := processor.ProcessPendingNotifications(ctx, handlerCtx, reader)
|
|
if err != nil {
|
|
t.Errorf("ProcessPendingNotifications should handle non-string type gracefully: %v", err)
|
|
}
|
|
|
|
handled := handler.GetHandledNotifications()
|
|
if len(handled) != 0 {
|
|
t.Errorf("Expected 0 handled notifications for non-string type, got %d", len(handled))
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestVoidProcessorWithFakeBuffer tests VoidProcessor with fake RESP3 data
|
|
func TestVoidProcessorWithFakeBuffer(t *testing.T) {
|
|
t.Run("ProcessPushNotifications", func(t *testing.T) {
|
|
processor := NewVoidProcessor()
|
|
|
|
// Create buffer with multiple push notifications
|
|
buf := createMultipleNotifications(
|
|
[]string{"MOVING", "slot", "123"},
|
|
[]string{"MIGRATING", "slot", "456"},
|
|
[]string{"FAILED_OVER", "node", "node1"},
|
|
)
|
|
reader := proto.NewReader(buf)
|
|
|
|
ctx := context.Background()
|
|
handlerCtx := NotificationHandlerContext{
|
|
Client: nil,
|
|
ConnPool: nil,
|
|
PubSub: nil,
|
|
Conn: nil,
|
|
IsBlocking: false,
|
|
}
|
|
|
|
err := processor.ProcessPendingNotifications(ctx, handlerCtx, reader)
|
|
if err != nil {
|
|
t.Errorf("VoidProcessor ProcessPendingNotifications should not error: %v", err)
|
|
}
|
|
|
|
// VoidProcessor should discard all notifications without processing
|
|
// We can't directly verify this, but the fact that it doesn't error is good
|
|
})
|
|
|
|
t.Run("ProcessSkippedNotifications", func(t *testing.T) {
|
|
processor := NewVoidProcessor()
|
|
|
|
// Create buffer with pub/sub notifications (should be skipped)
|
|
buf := createMultipleNotifications(
|
|
[]string{"message", "channel", "data"},
|
|
[]string{"pmessage", "pattern", "channel", "data"},
|
|
[]string{"subscribe", "channel", "1"},
|
|
)
|
|
reader := proto.NewReader(buf)
|
|
|
|
ctx := context.Background()
|
|
handlerCtx := NotificationHandlerContext{
|
|
Client: nil,
|
|
ConnPool: nil,
|
|
PubSub: nil,
|
|
Conn: nil,
|
|
IsBlocking: false,
|
|
}
|
|
|
|
err := processor.ProcessPendingNotifications(ctx, handlerCtx, reader)
|
|
if err != nil {
|
|
t.Errorf("VoidProcessor ProcessPendingNotifications should not error: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("ProcessMixedNotifications", func(t *testing.T) {
|
|
processor := NewVoidProcessor()
|
|
|
|
// Create buffer with mixed push notifications and regular arrays
|
|
buf := &bytes.Buffer{}
|
|
|
|
// Add push notification
|
|
pushBuf := createFakeRESP3PushNotification("MOVING", "slot", "123")
|
|
buf.Write(pushBuf.Bytes())
|
|
|
|
// Add regular array (should stop processing)
|
|
arrayBuf := createFakeRESP3Array("SOME", "COMMAND")
|
|
buf.Write(arrayBuf.Bytes())
|
|
|
|
reader := proto.NewReader(buf)
|
|
|
|
ctx := context.Background()
|
|
handlerCtx := NotificationHandlerContext{
|
|
Client: nil,
|
|
ConnPool: nil,
|
|
PubSub: nil,
|
|
Conn: nil,
|
|
IsBlocking: false,
|
|
}
|
|
|
|
err := processor.ProcessPendingNotifications(ctx, handlerCtx, reader)
|
|
if err != nil {
|
|
t.Errorf("VoidProcessor ProcessPendingNotifications should not error: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("ProcessInvalidNotificationFormat", func(t *testing.T) {
|
|
processor := NewVoidProcessor()
|
|
|
|
// Create invalid RESP3 data
|
|
buf := &bytes.Buffer{}
|
|
fmt.Fprint(buf, ">1\r\n") // Push notification with 1 element
|
|
fmt.Fprint(buf, "invalid\r\n") // Invalid format (should be $<len>\r\n<data>\r\n)
|
|
reader := proto.NewReader(buf)
|
|
|
|
ctx := context.Background()
|
|
handlerCtx := NotificationHandlerContext{
|
|
Client: nil,
|
|
ConnPool: nil,
|
|
PubSub: nil,
|
|
Conn: nil,
|
|
IsBlocking: false,
|
|
}
|
|
|
|
err := processor.ProcessPendingNotifications(ctx, handlerCtx, reader)
|
|
// VoidProcessor should handle errors gracefully
|
|
if err != nil {
|
|
t.Logf("VoidProcessor handled error gracefully: %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestProcessorErrorHandling tests error handling scenarios
|
|
func TestProcessorErrorHandling(t *testing.T) {
|
|
t.Run("ProcessWithEmptyBuffer", func(t *testing.T) {
|
|
processor := NewProcessor()
|
|
handler := NewTestHandler("test")
|
|
processor.RegisterHandler("MOVING", handler, false)
|
|
|
|
// Create empty buffer
|
|
buf := &bytes.Buffer{}
|
|
reader := proto.NewReader(buf)
|
|
|
|
ctx := context.Background()
|
|
handlerCtx := NotificationHandlerContext{
|
|
Client: nil,
|
|
ConnPool: nil,
|
|
PubSub: nil,
|
|
Conn: nil,
|
|
IsBlocking: false,
|
|
}
|
|
|
|
err := processor.ProcessPendingNotifications(ctx, handlerCtx, reader)
|
|
if err != nil {
|
|
t.Errorf("ProcessPendingNotifications should handle empty buffer gracefully: %v", err)
|
|
}
|
|
|
|
handled := handler.GetHandledNotifications()
|
|
if len(handled) != 0 {
|
|
t.Errorf("Expected 0 handled notifications for empty buffer, got %d", len(handled))
|
|
}
|
|
})
|
|
|
|
t.Run("ProcessWithCorruptedData", func(t *testing.T) {
|
|
processor := NewProcessor()
|
|
handler := NewTestHandler("test")
|
|
processor.RegisterHandler("MOVING", handler, false)
|
|
|
|
// Create buffer with corrupted RESP3 data
|
|
buf := &bytes.Buffer{}
|
|
fmt.Fprint(buf, ">2\r\n") // Says 2 elements
|
|
fmt.Fprint(buf, "$6\r\nMOVING\r\n") // First element OK
|
|
fmt.Fprint(buf, "corrupted") // Second element corrupted (no proper format)
|
|
reader := proto.NewReader(buf)
|
|
|
|
ctx := context.Background()
|
|
handlerCtx := NotificationHandlerContext{
|
|
Client: nil,
|
|
ConnPool: nil,
|
|
PubSub: nil,
|
|
Conn: nil,
|
|
IsBlocking: false,
|
|
}
|
|
|
|
err := processor.ProcessPendingNotifications(ctx, handlerCtx, reader)
|
|
// Should handle corruption gracefully
|
|
if err != nil {
|
|
t.Logf("Processor handled corrupted data gracefully: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("ProcessWithPartialData", func(t *testing.T) {
|
|
processor := NewProcessor()
|
|
handler := NewTestHandler("test")
|
|
processor.RegisterHandler("MOVING", handler, false)
|
|
|
|
// Create buffer with partial RESP3 data
|
|
buf := &bytes.Buffer{}
|
|
fmt.Fprint(buf, ">2\r\n") // Says 2 elements
|
|
fmt.Fprint(buf, "$6\r\nMOVING\r\n") // First element OK
|
|
// Missing second element
|
|
reader := proto.NewReader(buf)
|
|
|
|
ctx := context.Background()
|
|
handlerCtx := NotificationHandlerContext{
|
|
Client: nil,
|
|
ConnPool: nil,
|
|
PubSub: nil,
|
|
Conn: nil,
|
|
IsBlocking: false,
|
|
}
|
|
|
|
err := processor.ProcessPendingNotifications(ctx, handlerCtx, reader)
|
|
// Should handle partial data gracefully
|
|
if err != nil {
|
|
t.Logf("Processor handled partial data gracefully: %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestProcessorPerformanceWithFakeData tests performance with realistic data
|
|
func TestProcessorPerformanceWithFakeData(t *testing.T) {
|
|
processor := NewProcessor()
|
|
handler := NewTestHandler("test")
|
|
processor.RegisterHandler("MOVING", handler, false)
|
|
processor.RegisterHandler("MIGRATING", handler, false)
|
|
processor.RegisterHandler("MIGRATED", handler, false)
|
|
|
|
// Create buffer with many notifications
|
|
notifications := make([][]string, 100)
|
|
for i := 0; i < 100; i++ {
|
|
switch i % 3 {
|
|
case 0:
|
|
notifications[i] = []string{"MOVING", "slot", fmt.Sprintf("%d", i), "from", "node1", "to", "node2"}
|
|
case 1:
|
|
notifications[i] = []string{"MIGRATING", "slot", fmt.Sprintf("%d", i), "from", "node2", "to", "node3"}
|
|
case 2:
|
|
notifications[i] = []string{"MIGRATED", "slot", fmt.Sprintf("%d", i), "from", "node3", "to", "node1"}
|
|
}
|
|
}
|
|
|
|
buf := createMultipleNotifications(notifications...)
|
|
reader := proto.NewReader(buf)
|
|
|
|
ctx := context.Background()
|
|
handlerCtx := NotificationHandlerContext{
|
|
Client: nil,
|
|
ConnPool: nil,
|
|
PubSub: nil,
|
|
Conn: createMockConnection(),
|
|
IsBlocking: false,
|
|
}
|
|
|
|
err := processor.ProcessPendingNotifications(ctx, handlerCtx, reader)
|
|
if err != nil {
|
|
t.Errorf("ProcessPendingNotifications should not error with many notifications: %v", err)
|
|
}
|
|
|
|
handled := handler.GetHandledNotifications()
|
|
if len(handled) != 100 {
|
|
t.Errorf("Expected 100 handled notifications, got %d", len(handled))
|
|
}
|
|
}
|
|
|
|
// TestInterfaceCompliance tests that all types implement their interfaces correctly
|
|
func TestInterfaceCompliance(t *testing.T) {
|
|
// Test that Processor implements NotificationProcessor
|
|
var _ NotificationProcessor = (*Processor)(nil)
|
|
|
|
// Test that VoidProcessor implements NotificationProcessor
|
|
var _ NotificationProcessor = (*VoidProcessor)(nil)
|
|
|
|
// Test that NotificationHandlerContext is a concrete struct (no interface needed)
|
|
var _ NotificationHandlerContext = NotificationHandlerContext{}
|
|
|
|
// Test that TestHandler implements NotificationHandler
|
|
var _ NotificationHandler = (*TestHandler)(nil)
|
|
|
|
// Test that error types implement error interface
|
|
var _ error = (*HandlerError)(nil)
|
|
var _ error = (*ProcessorError)(nil)
|
|
}
|
|
|
|
// TestErrors tests the error definitions and helper functions
|
|
func TestErrors(t *testing.T) {
|
|
t.Run("ErrHandlerNil", func(t *testing.T) {
|
|
err := ErrHandlerNil
|
|
if err == nil {
|
|
t.Error("ErrHandlerNil should not be nil")
|
|
}
|
|
|
|
if err.Error() != "handler cannot be nil" {
|
|
t.Errorf("ErrHandlerNil message should be 'handler cannot be nil', got: %s", err.Error())
|
|
}
|
|
})
|
|
|
|
t.Run("ErrHandlerExists", func(t *testing.T) {
|
|
notificationName := "TEST_NOTIFICATION"
|
|
err := ErrHandlerExists(notificationName)
|
|
|
|
if err == nil {
|
|
t.Error("ErrHandlerExists should not return nil")
|
|
}
|
|
|
|
expectedMsg := "handler register failed for 'TEST_NOTIFICATION': cannot overwrite existing handler"
|
|
if err.Error() != expectedMsg {
|
|
t.Errorf("ErrHandlerExists message should be '%s', got: %s", expectedMsg, err.Error())
|
|
}
|
|
})
|
|
|
|
t.Run("ErrProtectedHandler", func(t *testing.T) {
|
|
notificationName := "PROTECTED_NOTIFICATION"
|
|
err := ErrProtectedHandler(notificationName)
|
|
|
|
if err == nil {
|
|
t.Error("ErrProtectedHandler should not return nil")
|
|
}
|
|
|
|
expectedMsg := "handler unregister failed for 'PROTECTED_NOTIFICATION': handler is protected"
|
|
if err.Error() != expectedMsg {
|
|
t.Errorf("ErrProtectedHandler message should be '%s', got: %s", expectedMsg, err.Error())
|
|
}
|
|
})
|
|
|
|
t.Run("ErrVoidProcessorRegister", func(t *testing.T) {
|
|
notificationName := "VOID_TEST"
|
|
err := ErrVoidProcessorRegister(notificationName)
|
|
|
|
if err == nil {
|
|
t.Error("ErrVoidProcessorRegister should not return nil")
|
|
}
|
|
|
|
expectedMsg := "void_processor register failed for 'VOID_TEST': push notifications are disabled"
|
|
if err.Error() != expectedMsg {
|
|
t.Errorf("ErrVoidProcessorRegister message should be '%s', got: %s", expectedMsg, err.Error())
|
|
}
|
|
})
|
|
|
|
t.Run("ErrVoidProcessorUnregister", func(t *testing.T) {
|
|
notificationName := "VOID_TEST"
|
|
err := ErrVoidProcessorUnregister(notificationName)
|
|
|
|
if err == nil {
|
|
t.Error("ErrVoidProcessorUnregister should not return nil")
|
|
}
|
|
|
|
expectedMsg := "void_processor unregister failed for 'VOID_TEST': push notifications are disabled"
|
|
if err.Error() != expectedMsg {
|
|
t.Errorf("ErrVoidProcessorUnregister message should be '%s', got: %s", expectedMsg, err.Error())
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestHandlerError tests the HandlerError structured error type
|
|
func TestHandlerError(t *testing.T) {
|
|
t.Run("HandlerErrorWithoutWrappedError", func(t *testing.T) {
|
|
err := NewHandlerError("register", "TEST_NOTIFICATION", "handler already exists", nil)
|
|
|
|
if err == nil {
|
|
t.Error("NewHandlerError should not return nil")
|
|
}
|
|
|
|
expectedMsg := "handler register failed for 'TEST_NOTIFICATION': handler already exists"
|
|
if err.Error() != expectedMsg {
|
|
t.Errorf("HandlerError message should be '%s', got: %s", expectedMsg, err.Error())
|
|
}
|
|
|
|
if err.Operation != "register" {
|
|
t.Errorf("HandlerError Operation should be 'register', got: %s", err.Operation)
|
|
}
|
|
|
|
if err.PushNotificationName != "TEST_NOTIFICATION" {
|
|
t.Errorf("HandlerError PushNotificationName should be 'TEST_NOTIFICATION', got: %s", err.PushNotificationName)
|
|
}
|
|
|
|
if err.Reason != "handler already exists" {
|
|
t.Errorf("HandlerError Reason should be 'handler already exists', got: %s", err.Reason)
|
|
}
|
|
|
|
if err.Unwrap() != nil {
|
|
t.Error("HandlerError Unwrap should return nil when no wrapped error")
|
|
}
|
|
})
|
|
|
|
t.Run("HandlerErrorWithWrappedError", func(t *testing.T) {
|
|
wrappedErr := errors.New("underlying error")
|
|
err := NewHandlerError("unregister", "PROTECTED_NOTIFICATION", "protected handler", wrappedErr)
|
|
|
|
expectedMsg := "handler unregister failed for 'PROTECTED_NOTIFICATION': protected handler (underlying error)"
|
|
if err.Error() != expectedMsg {
|
|
t.Errorf("HandlerError message should be '%s', got: %s", expectedMsg, err.Error())
|
|
}
|
|
|
|
if err.Unwrap() != wrappedErr {
|
|
t.Error("HandlerError Unwrap should return the wrapped error")
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestProcessorError tests the ProcessorError structured error type
|
|
func TestProcessorError(t *testing.T) {
|
|
t.Run("ProcessorErrorWithoutWrappedError", func(t *testing.T) {
|
|
err := NewProcessorError("processor", "process", "", "invalid notification format", nil)
|
|
|
|
if err == nil {
|
|
t.Error("NewProcessorError should not return nil")
|
|
}
|
|
|
|
expectedMsg := "processor process failed: invalid notification format"
|
|
if err.Error() != expectedMsg {
|
|
t.Errorf("ProcessorError message should be '%s', got: %s", expectedMsg, err.Error())
|
|
}
|
|
|
|
if err.ProcessorType != "processor" {
|
|
t.Errorf("ProcessorError ProcessorType should be 'processor', got: %s", err.ProcessorType)
|
|
}
|
|
|
|
if err.Operation != "process" {
|
|
t.Errorf("ProcessorError Operation should be 'process', got: %s", err.Operation)
|
|
}
|
|
|
|
if err.Reason != "invalid notification format" {
|
|
t.Errorf("ProcessorError Reason should be 'invalid notification format', got: %s", err.Reason)
|
|
}
|
|
|
|
if err.Unwrap() != nil {
|
|
t.Error("ProcessorError Unwrap should return nil when no wrapped error")
|
|
}
|
|
})
|
|
|
|
t.Run("ProcessorErrorWithWrappedError", func(t *testing.T) {
|
|
wrappedErr := errors.New("network error")
|
|
err := NewProcessorError("void_processor", "register", "", "disabled", wrappedErr)
|
|
|
|
expectedMsg := "void_processor register failed: disabled (network error)"
|
|
if err.Error() != expectedMsg {
|
|
t.Errorf("ProcessorError message should be '%s', got: %s", expectedMsg, err.Error())
|
|
}
|
|
|
|
if err.Unwrap() != wrappedErr {
|
|
t.Error("ProcessorError Unwrap should return the wrapped error")
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestErrorHelperFunctions tests the error checking helper functions
|
|
func TestErrorHelperFunctions(t *testing.T) {
|
|
t.Run("IsHandlerNilError", func(t *testing.T) {
|
|
// Test with ErrHandlerNil
|
|
if !IsHandlerNilError(ErrHandlerNil) {
|
|
t.Error("IsHandlerNilError should return true for ErrHandlerNil")
|
|
}
|
|
|
|
// Test with other error
|
|
otherErr := ErrHandlerExists("TEST")
|
|
if IsHandlerNilError(otherErr) {
|
|
t.Error("IsHandlerNilError should return false for other errors")
|
|
}
|
|
|
|
// Test with nil
|
|
if IsHandlerNilError(nil) {
|
|
t.Error("IsHandlerNilError should return false for nil")
|
|
}
|
|
})
|
|
|
|
t.Run("IsVoidProcessorError", func(t *testing.T) {
|
|
// Test with void processor register error
|
|
registerErr := ErrVoidProcessorRegister("TEST")
|
|
if !IsVoidProcessorError(registerErr) {
|
|
t.Error("IsVoidProcessorError should return true for void processor register error")
|
|
}
|
|
|
|
// Test with void processor unregister error
|
|
unregisterErr := ErrVoidProcessorUnregister("TEST")
|
|
if !IsVoidProcessorError(unregisterErr) {
|
|
t.Error("IsVoidProcessorError should return true for void processor unregister error")
|
|
}
|
|
|
|
// Test with other error
|
|
otherErr := ErrHandlerNil
|
|
if IsVoidProcessorError(otherErr) {
|
|
t.Error("IsVoidProcessorError should return false for other errors")
|
|
}
|
|
|
|
// Test with nil
|
|
if IsVoidProcessorError(nil) {
|
|
t.Error("IsVoidProcessorError should return false for nil")
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestErrorConstants tests the error reason constants
|
|
func TestErrorConstants(t *testing.T) {
|
|
t.Run("ErrorReasonConstants", func(t *testing.T) {
|
|
if ReasonHandlerNil != "handler cannot be nil" {
|
|
t.Errorf("ReasonHandlerNil should be 'handler cannot be nil', got: %s", ReasonHandlerNil)
|
|
}
|
|
|
|
if ReasonHandlerExists != "cannot overwrite existing handler" {
|
|
t.Errorf("ReasonHandlerExists should be 'cannot overwrite existing handler', got: %s", ReasonHandlerExists)
|
|
}
|
|
|
|
if ReasonHandlerProtected != "handler is protected" {
|
|
t.Errorf("ReasonHandlerProtected should be 'handler is protected', got: %s", ReasonHandlerProtected)
|
|
}
|
|
|
|
if ReasonPushNotificationsDisabled != "push notifications are disabled" {
|
|
t.Errorf("ReasonPushNotificationsDisabled should be 'push notifications are disabled', got: %s", ReasonPushNotificationsDisabled)
|
|
}
|
|
})
|
|
}
|
|
|
|
// Benchmark tests for performance
|
|
func BenchmarkRegistry(b *testing.B) {
|
|
registry := NewRegistry()
|
|
handler := NewTestHandler("test")
|
|
|
|
b.Run("RegisterHandler", func(b *testing.B) {
|
|
for i := 0; i < b.N; i++ {
|
|
registry.RegisterHandler("TEST", handler, false)
|
|
}
|
|
})
|
|
|
|
b.Run("GetHandler", func(b *testing.B) {
|
|
registry.RegisterHandler("TEST", handler, false)
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
registry.GetHandler("TEST")
|
|
}
|
|
})
|
|
}
|
|
|
|
func BenchmarkProcessor(b *testing.B) {
|
|
processor := NewProcessor()
|
|
handler := NewTestHandler("test")
|
|
processor.RegisterHandler("MOVING", handler, false)
|
|
|
|
b.Run("RegisterHandler", func(b *testing.B) {
|
|
for i := 0; i < b.N; i++ {
|
|
processor.RegisterHandler("TEST", handler, false)
|
|
}
|
|
})
|
|
|
|
b.Run("GetHandler", func(b *testing.B) {
|
|
for i := 0; i < b.N; i++ {
|
|
processor.GetHandler("MOVING")
|
|
}
|
|
})
|
|
}
|