package proto import ( "errors" "fmt" "testing" ) // TestTypedRedisErrors tests that typed Redis errors are created correctly func TestTypedRedisErrors(t *testing.T) { tests := []struct { name string errorMsg string expectedType interface{} expectedMsg string checkFunc func(error) bool extractAddr func(error) string }{ { name: "LOADING error", errorMsg: "LOADING Redis is loading the dataset in memory", expectedType: &LoadingError{}, expectedMsg: "LOADING Redis is loading the dataset in memory", checkFunc: IsLoadingError, }, { name: "READONLY error", errorMsg: "READONLY You can't write against a read only replica", expectedType: &ReadOnlyError{}, expectedMsg: "READONLY You can't write against a read only replica", checkFunc: IsReadOnlyError, }, { name: "MOVED error", errorMsg: "MOVED 3999 127.0.0.1:6381", expectedType: &MovedError{}, expectedMsg: "MOVED 3999 127.0.0.1:6381", checkFunc: func(err error) bool { _, ok := IsMovedError(err) return ok }, extractAddr: func(err error) string { if movedErr, ok := IsMovedError(err); ok { return movedErr.Addr() } return "" }, }, { name: "ASK error", errorMsg: "ASK 3999 127.0.0.1:6381", expectedType: &AskError{}, expectedMsg: "ASK 3999 127.0.0.1:6381", checkFunc: func(err error) bool { _, ok := IsAskError(err) return ok }, extractAddr: func(err error) string { if askErr, ok := IsAskError(err); ok { return askErr.Addr() } return "" }, }, { name: "CLUSTERDOWN error", errorMsg: "CLUSTERDOWN The cluster is down", expectedType: &ClusterDownError{}, expectedMsg: "CLUSTERDOWN The cluster is down", checkFunc: IsClusterDownError, }, { name: "TRYAGAIN error", errorMsg: "TRYAGAIN Multiple keys request during rehashing of slot", expectedType: &TryAgainError{}, expectedMsg: "TRYAGAIN Multiple keys request during rehashing of slot", checkFunc: IsTryAgainError, }, { name: "MASTERDOWN error", errorMsg: "MASTERDOWN Link with MASTER is down and replica-serve-stale-data is set to 'no'", expectedType: &MasterDownError{}, expectedMsg: "MASTERDOWN Link with MASTER is down and replica-serve-stale-data is set to 'no'", checkFunc: IsMasterDownError, }, { name: "Max clients error", errorMsg: "ERR max number of clients reached", expectedType: &MaxClientsError{}, expectedMsg: "ERR max number of clients reached", checkFunc: IsMaxClientsError, }, { name: "NOAUTH error", errorMsg: "NOAUTH Authentication required", expectedType: &AuthError{}, expectedMsg: "NOAUTH Authentication required", checkFunc: IsAuthError, }, { name: "WRONGPASS error", errorMsg: "WRONGPASS invalid username-password pair", expectedType: &AuthError{}, expectedMsg: "WRONGPASS invalid username-password pair", checkFunc: IsAuthError, }, { name: "unauthenticated error", errorMsg: "ERR unauthenticated", expectedType: &AuthError{}, expectedMsg: "ERR unauthenticated", checkFunc: IsAuthError, }, { name: "NOPERM error", errorMsg: "NOPERM this user has no permissions to run the 'flushdb' command", expectedType: &PermissionError{}, expectedMsg: "NOPERM this user has no permissions to run the 'flushdb' command", checkFunc: IsPermissionError, }, { name: "EXECABORT error", errorMsg: "EXECABORT Transaction discarded because of previous errors", expectedType: &ExecAbortError{}, expectedMsg: "EXECABORT Transaction discarded because of previous errors", checkFunc: IsExecAbortError, }, { name: "OOM error", errorMsg: "OOM command not allowed when used memory > 'maxmemory'", expectedType: &OOMError{}, expectedMsg: "OOM command not allowed when used memory > 'maxmemory'", checkFunc: IsOOMError, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := parseTypedRedisError(tt.errorMsg) // Check error message is preserved if err.Error() != tt.expectedMsg { t.Errorf("Error message mismatch: got %q, want %q", err.Error(), tt.expectedMsg) } // Check error type using errors.As if !errors.As(err, &tt.expectedType) { t.Errorf("Error type mismatch: expected %T, got %T", tt.expectedType, err) } // Check using the helper function if tt.checkFunc != nil && !tt.checkFunc(err) { t.Errorf("Helper function returned false for error: %v", err) } // Check address extraction for MOVED/ASK errors if tt.extractAddr != nil { addr := tt.extractAddr(err) if addr == "" { t.Errorf("Failed to extract address from error: %v", err) } } }) } } // TestWrappedTypedErrors tests that typed errors work correctly when wrapped func TestWrappedTypedErrors(t *testing.T) { tests := []struct { name string errorMsg string checkFunc func(error) bool }{ { name: "Wrapped LOADING error", errorMsg: "LOADING Redis is loading the dataset in memory", checkFunc: IsLoadingError, }, { name: "Wrapped READONLY error", errorMsg: "READONLY You can't write against a read only replica", checkFunc: IsReadOnlyError, }, { name: "Wrapped CLUSTERDOWN error", errorMsg: "CLUSTERDOWN The cluster is down", checkFunc: IsClusterDownError, }, { name: "Wrapped TRYAGAIN error", errorMsg: "TRYAGAIN Multiple keys request during rehashing of slot", checkFunc: IsTryAgainError, }, { name: "Wrapped MASTERDOWN error", errorMsg: "MASTERDOWN Link with MASTER is down", checkFunc: IsMasterDownError, }, { name: "Wrapped Max clients error", errorMsg: "ERR max number of clients reached", checkFunc: IsMaxClientsError, }, { name: "Wrapped NOAUTH error", errorMsg: "NOAUTH Authentication required", checkFunc: IsAuthError, }, { name: "Wrapped WRONGPASS error", errorMsg: "WRONGPASS invalid username-password pair", checkFunc: IsAuthError, }, { name: "Wrapped unauthenticated error", errorMsg: "ERR unauthenticated", checkFunc: IsAuthError, }, { name: "Wrapped NOPERM error", errorMsg: "NOPERM this user has no permissions to run the 'flushdb' command", checkFunc: IsPermissionError, }, { name: "Wrapped EXECABORT error", errorMsg: "EXECABORT Transaction discarded because of previous errors", checkFunc: IsExecAbortError, }, { name: "Wrapped OOM error", errorMsg: "OOM command not allowed when used memory > 'maxmemory'", checkFunc: IsOOMError, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Create the typed error err := parseTypedRedisError(tt.errorMsg) // Wrap it multiple times (simulating hook wrapping) wrappedErr := fmt.Errorf("hook error: %w", err) doubleWrappedErr := fmt.Errorf("another wrapper: %w", wrappedErr) // Check that the helper function still works with wrapped errors if !tt.checkFunc(doubleWrappedErr) { t.Errorf("Helper function failed to detect wrapped error: %v", doubleWrappedErr) } // Verify the original error message is still accessible if !errors.Is(doubleWrappedErr, err) { t.Errorf("errors.Is failed to match wrapped error") } }) } } // TestMovedAndAskErrorAddressExtraction tests address extraction from MOVED/ASK errors func TestMovedAndAskErrorAddressExtraction(t *testing.T) { tests := []struct { name string errorMsg string expectedAddr string }{ { name: "MOVED with IP address", errorMsg: "MOVED 3999 127.0.0.1:6381", expectedAddr: "127.0.0.1:6381", }, { name: "MOVED with hostname", errorMsg: "MOVED 3999 redis-node-1:6379", expectedAddr: "redis-node-1:6379", }, { name: "ASK with IP address", errorMsg: "ASK 3999 192.168.1.100:6380", expectedAddr: "192.168.1.100:6380", }, { name: "ASK with hostname", errorMsg: "ASK 3999 redis-node-2:6379", expectedAddr: "redis-node-2:6379", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := parseTypedRedisError(tt.errorMsg) var addr string if movedErr, ok := IsMovedError(err); ok { addr = movedErr.Addr() } else if askErr, ok := IsAskError(err); ok { addr = askErr.Addr() } else { t.Fatalf("Error is neither MOVED nor ASK: %v", err) } if addr != tt.expectedAddr { t.Errorf("Address mismatch: got %q, want %q", addr, tt.expectedAddr) } // Test with wrapped error wrappedErr := fmt.Errorf("wrapped: %w", err) if movedErr, ok := IsMovedError(wrappedErr); ok { addr = movedErr.Addr() } else if askErr, ok := IsAskError(wrappedErr); ok { addr = askErr.Addr() } else { t.Fatalf("Wrapped error is neither MOVED nor ASK: %v", wrappedErr) } if addr != tt.expectedAddr { t.Errorf("Address mismatch in wrapped error: got %q, want %q", addr, tt.expectedAddr) } }) } } // TestGenericRedisError tests that unknown Redis errors fall back to generic RedisError func TestGenericRedisError(t *testing.T) { tests := []struct { name string errorMsg string }{ { name: "Generic error", errorMsg: "ERR unknown command", }, { name: "WRONGTYPE error", errorMsg: "WRONGTYPE Operation against a key holding the wrong kind of value", }, { name: "BUSYKEY error", errorMsg: "BUSYKEY Target key name already exists", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := parseTypedRedisError(tt.errorMsg) // Should be a generic RedisError if _, ok := err.(RedisError); !ok { t.Errorf("Expected RedisError, got %T", err) } // Should preserve the error message if err.Error() != tt.errorMsg { t.Errorf("Error message mismatch: got %q, want %q", err.Error(), tt.errorMsg) } // Should not match any typed error checks if IsLoadingError(err) || IsReadOnlyError(err) || IsClusterDownError(err) || IsTryAgainError(err) || IsMasterDownError(err) || IsMaxClientsError(err) || IsAuthError(err) || IsPermissionError(err) || IsExecAbortError(err) || IsOOMError(err) { t.Errorf("Generic error incorrectly matched a typed error check") } }) } } // TestBackwardCompatibility tests that error messages remain unchanged func TestBackwardCompatibility(t *testing.T) { // This test ensures that the error messages are exactly the same as before // to maintain backward compatibility with code that checks error messages tests := []struct { input string expected string }{ {"LOADING Redis is loading the dataset in memory", "LOADING Redis is loading the dataset in memory"}, {"READONLY You can't write against a read only replica", "READONLY You can't write against a read only replica"}, {"MOVED 3999 127.0.0.1:6381", "MOVED 3999 127.0.0.1:6381"}, {"ASK 3999 127.0.0.1:6381", "ASK 3999 127.0.0.1:6381"}, {"CLUSTERDOWN The cluster is down", "CLUSTERDOWN The cluster is down"}, {"TRYAGAIN Multiple keys request during rehashing of slot", "TRYAGAIN Multiple keys request during rehashing of slot"}, {"MASTERDOWN Link with MASTER is down", "MASTERDOWN Link with MASTER is down"}, {"ERR max number of clients reached", "ERR max number of clients reached"}, } for _, tt := range tests { t.Run(tt.input, func(t *testing.T) { err := parseTypedRedisError(tt.input) if err.Error() != tt.expected { t.Errorf("Error message changed! Got %q, want %q", err.Error(), tt.expected) } }) } }