|  | 
|  | 1 | +package mcp | 
|  | 2 | + | 
|  | 3 | +import ( | 
|  | 4 | +	"errors" | 
|  | 5 | +	"testing" | 
|  | 6 | + | 
|  | 7 | +	"github.com/stretchr/testify/assert" | 
|  | 8 | +	"github.com/stretchr/testify/require" | 
|  | 9 | +) | 
|  | 10 | + | 
|  | 11 | +func TestUnsupportedProtocolVersionError_Is(t *testing.T) { | 
|  | 12 | +	err1 := UnsupportedProtocolVersionError{Version: "1.0"} | 
|  | 13 | +	err2 := UnsupportedProtocolVersionError{Version: "2.0"} | 
|  | 14 | + | 
|  | 15 | +	t.Run("matches same type", func(t *testing.T) { | 
|  | 16 | +		assert.True(t, err1.Is(UnsupportedProtocolVersionError{})) | 
|  | 17 | +		assert.True(t, err2.Is(UnsupportedProtocolVersionError{Version: "different"})) | 
|  | 18 | +	}) | 
|  | 19 | + | 
|  | 20 | +	t.Run("does not match different type", func(t *testing.T) { | 
|  | 21 | +		assert.False(t, err1.Is(errors.New("different error"))) | 
|  | 22 | +		assert.False(t, err1.Is(ErrMethodNotFound)) | 
|  | 23 | +	}) | 
|  | 24 | +} | 
|  | 25 | + | 
|  | 26 | +func TestIsUnsupportedProtocolVersion(t *testing.T) { | 
|  | 27 | +	t.Run("returns true for UnsupportedProtocolVersionError", func(t *testing.T) { | 
|  | 28 | +		err := UnsupportedProtocolVersionError{Version: "1.0"} | 
|  | 29 | +		assert.True(t, IsUnsupportedProtocolVersion(err)) | 
|  | 30 | +	}) | 
|  | 31 | + | 
|  | 32 | +	t.Run("returns false for other errors", func(t *testing.T) { | 
|  | 33 | +		assert.False(t, IsUnsupportedProtocolVersion(errors.New("other error"))) | 
|  | 34 | +		assert.False(t, IsUnsupportedProtocolVersion(ErrMethodNotFound)) | 
|  | 35 | +	}) | 
|  | 36 | + | 
|  | 37 | +	t.Run("returns false for wrapped errors", func(t *testing.T) { | 
|  | 38 | +		// Create a wrapped error - IsUnsupportedProtocolVersion checks direct type, not wrapped | 
|  | 39 | +		err := UnsupportedProtocolVersionError{Version: "1.0"} | 
|  | 40 | +		wrapped := errors.New("wrapped: " + err.Error()) | 
|  | 41 | +		assert.False(t, IsUnsupportedProtocolVersion(wrapped)) | 
|  | 42 | +	}) | 
|  | 43 | +} | 
|  | 44 | + | 
|  | 45 | +func TestJSONRPCErrorDetails_AsError_EmptyMessage(t *testing.T) { | 
|  | 46 | +	t.Run("with empty message", func(t *testing.T) { | 
|  | 47 | +		details := JSONRPCErrorDetails{ | 
|  | 48 | +			Code:    METHOD_NOT_FOUND, | 
|  | 49 | +			Message: "", | 
|  | 50 | +		} | 
|  | 51 | + | 
|  | 52 | +		err := details.AsError() | 
|  | 53 | +		// Should return the sentinel error when message is empty | 
|  | 54 | +		assert.Equal(t, ErrMethodNotFound, err) | 
|  | 55 | +	}) | 
|  | 56 | + | 
|  | 57 | +	t.Run("with message matching sentinel", func(t *testing.T) { | 
|  | 58 | +		details := JSONRPCErrorDetails{ | 
|  | 59 | +			Code:    PARSE_ERROR, | 
|  | 60 | +			Message: "parse error", | 
|  | 61 | +		} | 
|  | 62 | + | 
|  | 63 | +		err := details.AsError() | 
|  | 64 | +		assert.Equal(t, ErrParseError, err) | 
|  | 65 | +	}) | 
|  | 66 | +} | 
|  | 67 | + | 
|  | 68 | +func TestJSONRPCErrorDetails_AsError_AllCodes(t *testing.T) { | 
|  | 69 | +	tests := []struct { | 
|  | 70 | +		name        string | 
|  | 71 | +		code        int | 
|  | 72 | +		message     string | 
|  | 73 | +		sentinel    error | 
|  | 74 | +		shouldMatch bool | 
|  | 75 | +	}{ | 
|  | 76 | +		{ | 
|  | 77 | +			name:        "PARSE_ERROR", | 
|  | 78 | +			code:        PARSE_ERROR, | 
|  | 79 | +			message:     "custom parse error", | 
|  | 80 | +			sentinel:    ErrParseError, | 
|  | 81 | +			shouldMatch: true, | 
|  | 82 | +		}, | 
|  | 83 | +		{ | 
|  | 84 | +			name:        "INVALID_REQUEST", | 
|  | 85 | +			code:        INVALID_REQUEST, | 
|  | 86 | +			message:     "custom invalid request", | 
|  | 87 | +			sentinel:    ErrInvalidRequest, | 
|  | 88 | +			shouldMatch: true, | 
|  | 89 | +		}, | 
|  | 90 | +		{ | 
|  | 91 | +			name:        "METHOD_NOT_FOUND", | 
|  | 92 | +			code:        METHOD_NOT_FOUND, | 
|  | 93 | +			message:     "custom method not found", | 
|  | 94 | +			sentinel:    ErrMethodNotFound, | 
|  | 95 | +			shouldMatch: true, | 
|  | 96 | +		}, | 
|  | 97 | +		{ | 
|  | 98 | +			name:        "INVALID_PARAMS", | 
|  | 99 | +			code:        INVALID_PARAMS, | 
|  | 100 | +			message:     "custom invalid params", | 
|  | 101 | +			sentinel:    ErrInvalidParams, | 
|  | 102 | +			shouldMatch: true, | 
|  | 103 | +		}, | 
|  | 104 | +		{ | 
|  | 105 | +			name:        "INTERNAL_ERROR", | 
|  | 106 | +			code:        INTERNAL_ERROR, | 
|  | 107 | +			message:     "custom internal error", | 
|  | 108 | +			sentinel:    ErrInternalError, | 
|  | 109 | +			shouldMatch: true, | 
|  | 110 | +		}, | 
|  | 111 | +		{ | 
|  | 112 | +			name:        "REQUEST_INTERRUPTED", | 
|  | 113 | +			code:        REQUEST_INTERRUPTED, | 
|  | 114 | +			message:     "custom interrupted", | 
|  | 115 | +			sentinel:    ErrRequestInterrupted, | 
|  | 116 | +			shouldMatch: true, | 
|  | 117 | +		}, | 
|  | 118 | +		{ | 
|  | 119 | +			name:        "RESOURCE_NOT_FOUND", | 
|  | 120 | +			code:        RESOURCE_NOT_FOUND, | 
|  | 121 | +			message:     "custom resource not found", | 
|  | 122 | +			sentinel:    ErrResourceNotFound, | 
|  | 123 | +			shouldMatch: true, | 
|  | 124 | +		}, | 
|  | 125 | +		{ | 
|  | 126 | +			name:        "unknown code", | 
|  | 127 | +			code:        -99999, | 
|  | 128 | +			message:     "unknown error", | 
|  | 129 | +			sentinel:    nil, | 
|  | 130 | +			shouldMatch: false, | 
|  | 131 | +		}, | 
|  | 132 | +	} | 
|  | 133 | + | 
|  | 134 | +	for _, tt := range tests { | 
|  | 135 | +		t.Run(tt.name, func(t *testing.T) { | 
|  | 136 | +			details := JSONRPCErrorDetails{ | 
|  | 137 | +				Code:    tt.code, | 
|  | 138 | +				Message: tt.message, | 
|  | 139 | +			} | 
|  | 140 | + | 
|  | 141 | +			err := details.AsError() | 
|  | 142 | +			require.NotNil(t, err) | 
|  | 143 | + | 
|  | 144 | +			if tt.shouldMatch { | 
|  | 145 | +				assert.True(t, errors.Is(err, tt.sentinel)) | 
|  | 146 | +				// Custom message should be wrapped | 
|  | 147 | +				assert.Contains(t, err.Error(), tt.message) | 
|  | 148 | +			} else { | 
|  | 149 | +				// Unknown codes just return the message | 
|  | 150 | +				assert.Equal(t, tt.message, err.Error()) | 
|  | 151 | +			} | 
|  | 152 | +		}) | 
|  | 153 | +	} | 
|  | 154 | +} | 
|  | 155 | + | 
|  | 156 | +func TestErrorChaining_WithAs(t *testing.T) { | 
|  | 157 | +	t.Run("errors.As does not work with wrapped sentinel", func(t *testing.T) { | 
|  | 158 | +		details := &JSONRPCErrorDetails{ | 
|  | 159 | +			Code:    METHOD_NOT_FOUND, | 
|  | 160 | +			Message: "Method 'foo' not found", | 
|  | 161 | +		} | 
|  | 162 | + | 
|  | 163 | +		err := details.AsError() | 
|  | 164 | + | 
|  | 165 | +		// Since we wrap with fmt.Errorf, errors.As won't find the exact type | 
|  | 166 | +		// but errors.Is will work because of the %w verb | 
|  | 167 | +		assert.True(t, errors.Is(err, ErrMethodNotFound)) | 
|  | 168 | +	}) | 
|  | 169 | +} | 
|  | 170 | + | 
|  | 171 | +func TestSentinelErrors_Comparison(t *testing.T) { | 
|  | 172 | +	// Ensure all sentinel errors are distinct | 
|  | 173 | +	sentinels := []error{ | 
|  | 174 | +		ErrParseError, | 
|  | 175 | +		ErrInvalidRequest, | 
|  | 176 | +		ErrMethodNotFound, | 
|  | 177 | +		ErrInvalidParams, | 
|  | 178 | +		ErrInternalError, | 
|  | 179 | +		ErrRequestInterrupted, | 
|  | 180 | +		ErrResourceNotFound, | 
|  | 181 | +	} | 
|  | 182 | + | 
|  | 183 | +	for i, err1 := range sentinels { | 
|  | 184 | +		for j, err2 := range sentinels { | 
|  | 185 | +			if i == j { | 
|  | 186 | +				assert.True(t, errors.Is(err1, err2), "Same sentinel should match itself") | 
|  | 187 | +			} else { | 
|  | 188 | +				assert.False(t, errors.Is(err1, err2), "Different sentinels should not match") | 
|  | 189 | +			} | 
|  | 190 | +		} | 
|  | 191 | +	} | 
|  | 192 | +} | 
|  | 193 | + | 
|  | 194 | +func TestUnsupportedProtocolVersionError_Error(t *testing.T) { | 
|  | 195 | +	err := UnsupportedProtocolVersionError{Version: "3.0"} | 
|  | 196 | +	assert.Equal(t, `unsupported protocol version: "3.0"`, err.Error()) | 
|  | 197 | +} | 
|  | 198 | + | 
|  | 199 | +func TestJSONRPCErrorDetails_WithData(t *testing.T) { | 
|  | 200 | +	details := &JSONRPCErrorDetails{ | 
|  | 201 | +		Code:    INVALID_PARAMS, | 
|  | 202 | +		Message: "Invalid parameter 'foo'", | 
|  | 203 | +		Data: map[string]any{ | 
|  | 204 | +			"param":    "foo", | 
|  | 205 | +			"expected": "string", | 
|  | 206 | +			"got":      "number", | 
|  | 207 | +		}, | 
|  | 208 | +	} | 
|  | 209 | + | 
|  | 210 | +	err := details.AsError() | 
|  | 211 | + | 
|  | 212 | +	// The error should still wrap properly | 
|  | 213 | +	assert.True(t, errors.Is(err, ErrInvalidParams)) | 
|  | 214 | +	assert.Contains(t, err.Error(), "Invalid parameter 'foo'") | 
|  | 215 | + | 
|  | 216 | +	// Data is not included in the error string, but it's preserved in the details | 
|  | 217 | +	assert.NotNil(t, details.Data) | 
|  | 218 | +} | 
0 commit comments