mirror of
https://github.com/go-i2p/go-i2cp.git
synced 2025-12-01 06:54:57 -05:00
567 lines
14 KiB
Go
567 lines
14 KiB
Go
package go_i2cp
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// TestSentinelErrors verifies all sentinel errors are defined and have proper messages
|
|
func TestSentinelErrors(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
err error
|
|
wantMsg string
|
|
}{
|
|
{
|
|
name: "ErrSessionInvalid",
|
|
err: ErrSessionInvalid,
|
|
wantMsg: "session invalid or closed",
|
|
},
|
|
{
|
|
name: "ErrConnectionClosed",
|
|
err: ErrConnectionClosed,
|
|
wantMsg: "connection closed",
|
|
},
|
|
{
|
|
name: "ErrMessageTooLarge",
|
|
err: ErrMessageTooLarge,
|
|
wantMsg: "message exceeds size limit",
|
|
},
|
|
{
|
|
name: "ErrAuthenticationFailed",
|
|
err: ErrAuthenticationFailed,
|
|
wantMsg: "authentication failed",
|
|
},
|
|
{
|
|
name: "ErrProtocolVersion",
|
|
err: ErrProtocolVersion,
|
|
wantMsg: "unsupported protocol version",
|
|
},
|
|
{
|
|
name: "ErrTimeout",
|
|
err: ErrTimeout,
|
|
wantMsg: "operation timed out",
|
|
},
|
|
{
|
|
name: "ErrNoPrimarySession",
|
|
err: ErrNoPrimarySession,
|
|
wantMsg: "no primary session",
|
|
},
|
|
{
|
|
name: "ErrMultiSessionUnsupported",
|
|
err: ErrMultiSessionUnsupported,
|
|
wantMsg: "does not support multi-session",
|
|
},
|
|
{
|
|
name: "ErrInvalidDestination",
|
|
err: ErrInvalidDestination,
|
|
wantMsg: "invalid destination",
|
|
},
|
|
{
|
|
name: "ErrInvalidLeaseSet",
|
|
err: ErrInvalidLeaseSet,
|
|
wantMsg: "invalid leaseset",
|
|
},
|
|
{
|
|
name: "ErrMessageParsing",
|
|
err: ErrMessageParsing,
|
|
wantMsg: "message parsing failed",
|
|
},
|
|
{
|
|
name: "ErrInvalidSessionID",
|
|
err: ErrInvalidSessionID,
|
|
wantMsg: "invalid session id",
|
|
},
|
|
{
|
|
name: "ErrSessionRefused",
|
|
err: ErrSessionRefused,
|
|
wantMsg: "session creation refused",
|
|
},
|
|
{
|
|
name: "ErrNotConnected",
|
|
err: ErrNotConnected,
|
|
wantMsg: "not connected",
|
|
},
|
|
{
|
|
name: "ErrAlreadyConnected",
|
|
err: ErrAlreadyConnected,
|
|
wantMsg: "already connected",
|
|
},
|
|
{
|
|
name: "ErrInvalidConfiguration",
|
|
err: ErrInvalidConfiguration,
|
|
wantMsg: "invalid session configuration",
|
|
},
|
|
{
|
|
name: "ErrDestinationLookupFailed",
|
|
err: ErrDestinationLookupFailed,
|
|
wantMsg: "destination lookup failed",
|
|
},
|
|
{
|
|
name: "ErrBlindingRequired",
|
|
err: ErrBlindingRequired,
|
|
wantMsg: "blinding info required",
|
|
},
|
|
{
|
|
name: "ErrUnsupportedCrypto",
|
|
err: ErrUnsupportedCrypto,
|
|
wantMsg: "unsupported cryptographic algorithm",
|
|
},
|
|
{
|
|
name: "ErrInvalidSignature",
|
|
err: ErrInvalidSignature,
|
|
wantMsg: "invalid signature",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if tt.err == nil {
|
|
t.Errorf("%s is nil", tt.name)
|
|
}
|
|
if !strings.Contains(tt.err.Error(), tt.wantMsg) {
|
|
t.Errorf("%s message = %q, want to contain %q", tt.name, tt.err.Error(), tt.wantMsg)
|
|
}
|
|
// Verify all errors have "i2cp:" prefix for consistency
|
|
if !strings.HasPrefix(tt.err.Error(), "i2cp:") {
|
|
t.Errorf("%s message = %q, want prefix 'i2cp:'", tt.name, tt.err.Error())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestErrorWrapping verifies errors can be properly wrapped and unwrapped
|
|
func TestErrorWrapping(t *testing.T) {
|
|
baseErr := errors.New("base error")
|
|
|
|
tests := []struct {
|
|
name string
|
|
wrap error
|
|
want error
|
|
wantMsg string
|
|
}{
|
|
{
|
|
name: "wrap with fmt.Errorf",
|
|
wrap: fmt.Errorf("operation failed: %w", ErrSessionInvalid),
|
|
want: ErrSessionInvalid,
|
|
wantMsg: "operation failed",
|
|
},
|
|
{
|
|
name: "wrap base error with sentinel",
|
|
wrap: fmt.Errorf("%w: %s", ErrConnectionClosed, "network unreachable"),
|
|
want: ErrConnectionClosed,
|
|
wantMsg: "network unreachable",
|
|
},
|
|
{
|
|
name: "multiple wrapping levels",
|
|
wrap: fmt.Errorf("outer: %w", fmt.Errorf("inner: %w", baseErr)),
|
|
want: baseErr,
|
|
wantMsg: "outer",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Test errors.Is
|
|
if !errors.Is(tt.wrap, tt.want) {
|
|
t.Errorf("errors.Is(%v, %v) = false, want true", tt.wrap, tt.want)
|
|
}
|
|
|
|
// Test error message contains expected text
|
|
if !strings.Contains(tt.wrap.Error(), tt.wantMsg) {
|
|
t.Errorf("error message = %q, want to contain %q", tt.wrap.Error(), tt.wantMsg)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestMessageError verifies MessageError type functionality
|
|
func TestMessageError(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
messageType uint8
|
|
operation string
|
|
err error
|
|
wantMsgType uint8
|
|
wantOp string
|
|
wantContain string
|
|
}{
|
|
{
|
|
name: "message parsing error",
|
|
messageType: I2CP_MSG_CREATE_SESSION,
|
|
operation: "parsing",
|
|
err: errors.New("invalid format"),
|
|
wantMsgType: I2CP_MSG_CREATE_SESSION,
|
|
wantOp: "parsing",
|
|
wantContain: "invalid format",
|
|
},
|
|
{
|
|
name: "message sending error",
|
|
messageType: I2CP_MSG_SEND_MESSAGE,
|
|
operation: "sending",
|
|
err: ErrConnectionClosed,
|
|
wantMsgType: I2CP_MSG_SEND_MESSAGE,
|
|
wantOp: "sending",
|
|
wantContain: "connection closed",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := NewMessageError(tt.messageType, tt.operation, tt.err)
|
|
|
|
// Test type assertion
|
|
var msgErr *MessageError
|
|
if !errors.As(err, &msgErr) {
|
|
t.Fatalf("error is not a MessageError: %T", err)
|
|
}
|
|
|
|
// Test fields
|
|
if msgErr.MessageType != tt.wantMsgType {
|
|
t.Errorf("MessageType = %d, want %d", msgErr.MessageType, tt.wantMsgType)
|
|
}
|
|
if msgErr.Operation != tt.wantOp {
|
|
t.Errorf("Operation = %q, want %q", msgErr.Operation, tt.wantOp)
|
|
}
|
|
|
|
// Test error message
|
|
errMsg := err.Error()
|
|
if !strings.Contains(errMsg, tt.wantContain) {
|
|
t.Errorf("error message = %q, want to contain %q", errMsg, tt.wantContain)
|
|
}
|
|
|
|
// Test unwrapping
|
|
if !errors.Is(err, tt.err) {
|
|
t.Errorf("errors.Is(err, %v) = false, want true", tt.err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestSessionError verifies SessionError type functionality
|
|
func TestSessionError(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
sessionID uint16
|
|
operation string
|
|
err error
|
|
wantSessionID uint16
|
|
wantOp string
|
|
wantContain string
|
|
}{
|
|
{
|
|
name: "session creation error",
|
|
sessionID: 123,
|
|
operation: "creation",
|
|
err: ErrSessionRefused,
|
|
wantSessionID: 123,
|
|
wantOp: "creation",
|
|
wantContain: "refused",
|
|
},
|
|
{
|
|
name: "session message send error",
|
|
sessionID: 456,
|
|
operation: "send message",
|
|
err: errors.New("queue full"),
|
|
wantSessionID: 456,
|
|
wantOp: "send message",
|
|
wantContain: "queue full",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := NewSessionError(tt.sessionID, tt.operation, tt.err)
|
|
|
|
// Test type assertion
|
|
var sessErr *SessionError
|
|
if !errors.As(err, &sessErr) {
|
|
t.Fatalf("error is not a SessionError: %T", err)
|
|
}
|
|
|
|
// Test fields
|
|
if sessErr.SessionID != tt.wantSessionID {
|
|
t.Errorf("SessionID = %d, want %d", sessErr.SessionID, tt.wantSessionID)
|
|
}
|
|
if sessErr.Operation != tt.wantOp {
|
|
t.Errorf("Operation = %q, want %q", sessErr.Operation, tt.wantOp)
|
|
}
|
|
|
|
// Test error message
|
|
errMsg := err.Error()
|
|
if !strings.Contains(errMsg, tt.wantContain) {
|
|
t.Errorf("error message = %q, want to contain %q", errMsg, tt.wantContain)
|
|
}
|
|
|
|
// Test unwrapping
|
|
if !errors.Is(err, tt.err) {
|
|
t.Errorf("errors.Is(err, %v) = false, want true", tt.err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestProtocolError verifies ProtocolError type functionality
|
|
func TestProtocolError(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
message string
|
|
code int
|
|
fatal bool
|
|
wantContain string
|
|
wantFatal bool
|
|
}{
|
|
{
|
|
name: "non-fatal protocol error",
|
|
message: "unknown message type",
|
|
code: 99,
|
|
fatal: false,
|
|
wantContain: "unknown message type",
|
|
wantFatal: false,
|
|
},
|
|
{
|
|
name: "fatal protocol error",
|
|
message: "protocol violation",
|
|
code: 0,
|
|
fatal: true,
|
|
wantContain: "protocol violation",
|
|
wantFatal: true,
|
|
},
|
|
{
|
|
name: "error with code",
|
|
message: "invalid state",
|
|
code: 42,
|
|
fatal: false,
|
|
wantContain: "code 42",
|
|
wantFatal: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := NewProtocolError(tt.message, tt.code, tt.fatal)
|
|
|
|
// Test type assertion
|
|
var protoErr *ProtocolError
|
|
if !errors.As(err, &protoErr) {
|
|
t.Fatalf("error is not a ProtocolError: %T", err)
|
|
}
|
|
|
|
// Test fields
|
|
if protoErr.Message != tt.message {
|
|
t.Errorf("Message = %q, want %q", protoErr.Message, tt.message)
|
|
}
|
|
if protoErr.Code != tt.code {
|
|
t.Errorf("Code = %d, want %d", protoErr.Code, tt.code)
|
|
}
|
|
if protoErr.Fatal != tt.fatal {
|
|
t.Errorf("Fatal = %v, want %v", protoErr.Fatal, tt.fatal)
|
|
}
|
|
|
|
// Test error message
|
|
errMsg := err.Error()
|
|
if !strings.Contains(errMsg, tt.wantContain) {
|
|
t.Errorf("error message = %q, want to contain %q", errMsg, tt.wantContain)
|
|
}
|
|
|
|
// Test IsFatal
|
|
if IsFatal(err) != tt.wantFatal {
|
|
t.Errorf("IsFatal() = %v, want %v", IsFatal(err), tt.wantFatal)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestIsTemporary verifies IsTemporary correctly identifies temporary errors
|
|
func TestIsTemporary(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
err error
|
|
want bool
|
|
}{
|
|
{
|
|
name: "nil error",
|
|
err: nil,
|
|
want: false,
|
|
},
|
|
{
|
|
name: "timeout error is temporary",
|
|
err: ErrTimeout,
|
|
want: true,
|
|
},
|
|
{
|
|
name: "wrapped timeout error is temporary",
|
|
err: fmt.Errorf("operation: %w", ErrTimeout),
|
|
want: true,
|
|
},
|
|
{
|
|
name: "connection closed is not temporary",
|
|
err: ErrConnectionClosed,
|
|
want: false,
|
|
},
|
|
{
|
|
name: "protocol error is not temporary",
|
|
err: ErrProtocolVersion,
|
|
want: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := IsTemporary(tt.err)
|
|
if got != tt.want {
|
|
t.Errorf("IsTemporary(%v) = %v, want %v", tt.err, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestIsFatal verifies IsFatal correctly identifies fatal errors
|
|
func TestIsFatal(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
err error
|
|
want bool
|
|
}{
|
|
{
|
|
name: "nil error",
|
|
err: nil,
|
|
want: false,
|
|
},
|
|
{
|
|
name: "protocol version error is fatal",
|
|
err: ErrProtocolVersion,
|
|
want: true,
|
|
},
|
|
{
|
|
name: "authentication failure is fatal",
|
|
err: ErrAuthenticationFailed,
|
|
want: true,
|
|
},
|
|
{
|
|
name: "invalid signature is fatal",
|
|
err: ErrInvalidSignature,
|
|
want: true,
|
|
},
|
|
{
|
|
name: "wrapped fatal error is still fatal",
|
|
err: fmt.Errorf("connection: %w", ErrProtocolVersion),
|
|
want: true,
|
|
},
|
|
{
|
|
name: "protocol error with Fatal=true",
|
|
err: NewProtocolError("critical violation", 1, true),
|
|
want: true,
|
|
},
|
|
{
|
|
name: "protocol error with Fatal=false",
|
|
err: NewProtocolError("minor issue", 2, false),
|
|
want: false,
|
|
},
|
|
{
|
|
name: "timeout is not fatal",
|
|
err: ErrTimeout,
|
|
want: false,
|
|
},
|
|
{
|
|
name: "connection closed is not fatal",
|
|
err: ErrConnectionClosed,
|
|
want: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := IsFatal(tt.err)
|
|
if got != tt.want {
|
|
t.Errorf("IsFatal(%v) = %v, want %v", tt.err, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestErrorChaining verifies complex error wrapping scenarios
|
|
func TestErrorChaining(t *testing.T) {
|
|
// Create a chain: base -> MessageError -> SessionError -> wrapped
|
|
baseErr := errors.New("network error")
|
|
msgErr := NewMessageError(I2CP_MSG_SEND_MESSAGE, "sending", baseErr)
|
|
sessErr := NewSessionError(123, "message send", msgErr)
|
|
wrapped := fmt.Errorf("client operation failed: %w", sessErr)
|
|
|
|
// Test we can unwrap through the chain
|
|
if !errors.Is(wrapped, baseErr) {
|
|
t.Error("errors.Is failed to find base error through chain")
|
|
}
|
|
if !errors.Is(wrapped, ErrConnectionClosed) {
|
|
// This should be false - just verify behavior
|
|
}
|
|
|
|
// Test we can extract typed errors from chain
|
|
var extractedMsgErr *MessageError
|
|
if !errors.As(wrapped, &extractedMsgErr) {
|
|
t.Error("errors.As failed to extract MessageError from chain")
|
|
}
|
|
if extractedMsgErr.MessageType != I2CP_MSG_SEND_MESSAGE {
|
|
t.Errorf("extracted MessageError has wrong type: %d", extractedMsgErr.MessageType)
|
|
}
|
|
|
|
var extractedSessErr *SessionError
|
|
if !errors.As(wrapped, &extractedSessErr) {
|
|
t.Error("errors.As failed to extract SessionError from chain")
|
|
}
|
|
if extractedSessErr.SessionID != 123 {
|
|
t.Errorf("extracted SessionError has wrong ID: %d", extractedSessErr.SessionID)
|
|
}
|
|
}
|
|
|
|
// TestErrorConsistency ensures all error messages follow conventions
|
|
func TestErrorConsistency(t *testing.T) {
|
|
allErrors := []error{
|
|
ErrSessionInvalid,
|
|
ErrConnectionClosed,
|
|
ErrMessageTooLarge,
|
|
ErrAuthenticationFailed,
|
|
ErrProtocolVersion,
|
|
ErrTimeout,
|
|
ErrNoPrimarySession,
|
|
ErrMultiSessionUnsupported,
|
|
ErrInvalidDestination,
|
|
ErrInvalidLeaseSet,
|
|
ErrMessageParsing,
|
|
ErrInvalidSessionID,
|
|
ErrSessionRefused,
|
|
ErrNotConnected,
|
|
ErrAlreadyConnected,
|
|
ErrInvalidConfiguration,
|
|
ErrDestinationLookupFailed,
|
|
ErrBlindingRequired,
|
|
ErrUnsupportedCrypto,
|
|
ErrInvalidSignature,
|
|
}
|
|
|
|
for _, err := range allErrors {
|
|
errMsg := err.Error()
|
|
|
|
// All errors should have "i2cp:" prefix
|
|
if !strings.HasPrefix(errMsg, "i2cp:") {
|
|
t.Errorf("error %q missing 'i2cp:' prefix", errMsg)
|
|
}
|
|
|
|
// Error messages should be lowercase (after prefix)
|
|
parts := strings.SplitN(errMsg, ": ", 2)
|
|
if len(parts) == 2 {
|
|
msg := parts[1]
|
|
if msg != strings.ToLower(msg) {
|
|
t.Errorf("error message %q should be lowercase", errMsg)
|
|
}
|
|
}
|
|
|
|
// Error messages should not end with punctuation
|
|
if strings.HasSuffix(errMsg, ".") || strings.HasSuffix(errMsg, "!") {
|
|
t.Errorf("error message %q should not end with punctuation", errMsg)
|
|
}
|
|
}
|
|
}
|