1
0
mirror of https://github.com/redis/go-redis.git synced 2025-07-20 22:42:59 +03:00
Files
go-redis/push/push_test.go
2025-07-05 13:09:14 +03:00

1718 lines
51 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.Error("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(), "cannot unregister protected handler") {
t.Errorf("Error message should mention protected handler, 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.Error("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(), "cannot register push notification handler") {
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(), "cannot unregister push notification handler") {
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
buf.WriteString(fmt.Sprintf(">%d\r\n", totalElements))
// Write notification type as bulk string
buf.WriteString(fmt.Sprintf("$%d\r\n%s\r\n", len(notificationType), notificationType))
// Write arguments as bulk strings
for _, arg := range args {
buf.WriteString(fmt.Sprintf("$%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
buf.WriteString(fmt.Sprintf("*%d\r\n", len(elements)))
// Write elements as bulk strings
for _, element := range elements {
buf.WriteString(fmt.Sprintf("$%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{}
buf.WriteString(fmt.Sprintf("-%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{}
buf.WriteString(">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{}
buf.WriteString(">2\r\n") // 2 elements
buf.WriteString(":123\r\n") // Integer instead of string
buf.WriteString("$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{}
buf.WriteString(">1\r\n") // Push notification with 1 element
buf.WriteString("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{}
buf.WriteString(">2\r\n") // Says 2 elements
buf.WriteString("$6\r\nMOVING\r\n") // First element OK
buf.WriteString("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{}
buf.WriteString(">2\r\n") // Says 2 elements
buf.WriteString("$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 := "cannot overwrite existing handler for push notification: TEST_NOTIFICATION"
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 := "cannot unregister protected handler for push notification: PROTECTED_NOTIFICATION"
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 := "cannot register push notification handler 'VOID_TEST': push notifications are disabled (using void processor)"
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 := "cannot unregister push notification handler 'VOID_TEST': push notifications are disabled (using void processor)"
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 message constants
func TestErrorConstants(t *testing.T) {
t.Run("ErrorMessageConstants", func(t *testing.T) {
if MsgHandlerNil != "handler cannot be nil" {
t.Errorf("MsgHandlerNil should be 'handler cannot be nil', got: %s", MsgHandlerNil)
}
if MsgHandlerExists != "cannot overwrite existing handler for push notification: %s" {
t.Errorf("MsgHandlerExists should be 'cannot overwrite existing handler for push notification: %%s', got: %s", MsgHandlerExists)
}
if MsgProtectedHandler != "cannot unregister protected handler for push notification: %s" {
t.Errorf("MsgProtectedHandler should be 'cannot unregister protected handler for push notification: %%s', got: %s", MsgProtectedHandler)
}
if MsgVoidProcessorRegister != "cannot register push notification handler '%s': push notifications are disabled (using void processor)" {
t.Errorf("MsgVoidProcessorRegister constant mismatch, got: %s", MsgVoidProcessorRegister)
}
if MsgVoidProcessorUnregister != "cannot unregister push notification handler '%s': push notifications are disabled (using void processor)" {
t.Errorf("MsgVoidProcessorUnregister constant mismatch, got: %s", MsgVoidProcessorUnregister)
}
})
}
// 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")
}
})
}