Skip to content

Commit 9403161

Browse files
add test
1 parent f46f435 commit 9403161

File tree

1 file changed

+379
-0
lines changed

1 file changed

+379
-0
lines changed

pkg/errors/error_test.go

Lines changed: 379 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,379 @@
1+
package errors
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net/http"
7+
"testing"
8+
9+
"github.com/google/go-github/v72/github"
10+
"github.com/stretchr/testify/assert"
11+
"github.com/stretchr/testify/require"
12+
)
13+
14+
func TestGitHubErrorContext(t *testing.T) {
15+
t.Run("API errors can be added to context and retrieved", func(t *testing.T) {
16+
// Given a context with GitHub error tracking enabled
17+
ctx := ContextWithGitHubErrors(context.Background())
18+
19+
// Create a mock GitHub response
20+
resp := &github.Response{
21+
Response: &http.Response{
22+
StatusCode: 404,
23+
Status: "404 Not Found",
24+
},
25+
}
26+
originalErr := fmt.Errorf("resource not found")
27+
28+
// When we add an API error to the context
29+
updatedCtx, err := NewGitHubAPIErrorToCtx(ctx, "failed to fetch resource", resp, originalErr)
30+
require.NoError(t, err)
31+
32+
// Then we should be able to retrieve the error from the updated context
33+
apiErrors, err := GetGitHubAPIErrors(updatedCtx)
34+
require.NoError(t, err)
35+
require.Len(t, apiErrors, 1)
36+
37+
apiError := apiErrors[0]
38+
assert.Equal(t, "failed to fetch resource", apiError.Message)
39+
assert.Equal(t, resp, apiError.Response)
40+
assert.Equal(t, originalErr, apiError.Err)
41+
assert.Equal(t, "failed to fetch resource: resource not found", apiError.Error())
42+
})
43+
44+
t.Run("GraphQL errors can be added to context and retrieved", func(t *testing.T) {
45+
// Given a context with GitHub error tracking enabled
46+
ctx := ContextWithGitHubErrors(context.Background())
47+
48+
originalErr := fmt.Errorf("GraphQL query failed")
49+
50+
// When we add a GraphQL error to the context
51+
graphQLErr := newGitHubGraphQLError("failed to execute mutation", originalErr)
52+
updatedCtx, err := addGitHubGraphQLErrorToContext(ctx, graphQLErr)
53+
require.NoError(t, err)
54+
55+
// Then we should be able to retrieve the error from the updated context
56+
gqlErrors, err := GetGitHubGraphQLErrors(updatedCtx)
57+
require.NoError(t, err)
58+
require.Len(t, gqlErrors, 1)
59+
60+
gqlError := gqlErrors[0]
61+
assert.Equal(t, "failed to execute mutation", gqlError.Message)
62+
assert.Equal(t, originalErr, gqlError.Err)
63+
assert.Equal(t, "failed to execute mutation: GraphQL query failed", gqlError.Error())
64+
})
65+
66+
t.Run("multiple errors can be accumulated in context", func(t *testing.T) {
67+
// Given a context with GitHub error tracking enabled
68+
ctx := ContextWithGitHubErrors(context.Background())
69+
70+
// When we add multiple API errors
71+
resp1 := &github.Response{Response: &http.Response{StatusCode: 404}}
72+
resp2 := &github.Response{Response: &http.Response{StatusCode: 403}}
73+
74+
ctx, err := NewGitHubAPIErrorToCtx(ctx, "first error", resp1, fmt.Errorf("not found"))
75+
require.NoError(t, err)
76+
77+
ctx, err = NewGitHubAPIErrorToCtx(ctx, "second error", resp2, fmt.Errorf("forbidden"))
78+
require.NoError(t, err)
79+
80+
// And add a GraphQL error
81+
gqlErr := newGitHubGraphQLError("graphql error", fmt.Errorf("query failed"))
82+
ctx, err = addGitHubGraphQLErrorToContext(ctx, gqlErr)
83+
require.NoError(t, err)
84+
85+
// Then we should be able to retrieve all errors
86+
apiErrors, err := GetGitHubAPIErrors(ctx)
87+
require.NoError(t, err)
88+
assert.Len(t, apiErrors, 2)
89+
90+
gqlErrors, err := GetGitHubGraphQLErrors(ctx)
91+
require.NoError(t, err)
92+
assert.Len(t, gqlErrors, 1)
93+
94+
// Verify error details
95+
assert.Equal(t, "first error", apiErrors[0].Message)
96+
assert.Equal(t, "second error", apiErrors[1].Message)
97+
assert.Equal(t, "graphql error", gqlErrors[0].Message)
98+
})
99+
100+
t.Run("context pointer sharing allows middleware to inspect errors without context propagation", func(t *testing.T) {
101+
// This test demonstrates the key behavior: even when the context itself
102+
// isn't propagated through function calls, the pointer to the error slice
103+
// is shared, allowing middleware to inspect errors that were added later.
104+
105+
// Given a context with GitHub error tracking enabled
106+
originalCtx := ContextWithGitHubErrors(context.Background())
107+
108+
// Simulate a middleware that captures the context early
109+
var middlewareCtx context.Context
110+
111+
// Middleware function that captures the context
112+
middleware := func(ctx context.Context) {
113+
middlewareCtx = ctx // Middleware saves the context reference
114+
}
115+
116+
// Call middleware with the original context
117+
middleware(originalCtx)
118+
119+
// Simulate some business logic that adds errors to the context
120+
// but doesn't propagate the updated context back to middleware
121+
businessLogic := func(ctx context.Context) {
122+
resp := &github.Response{Response: &http.Response{StatusCode: 500}}
123+
124+
// Add an error to the context (this modifies the shared pointer)
125+
_, err := NewGitHubAPIErrorToCtx(ctx, "business logic failed", resp, fmt.Errorf("internal error"))
126+
require.NoError(t, err)
127+
128+
// Add another error
129+
_, err = NewGitHubAPIErrorToCtx(ctx, "second failure", resp, fmt.Errorf("another error"))
130+
require.NoError(t, err)
131+
}
132+
133+
// Execute business logic - note that we don't propagate the returned context
134+
businessLogic(originalCtx)
135+
136+
// Then the middleware should be able to see the errors that were added
137+
// even though it only has a reference to the original context
138+
apiErrors, err := GetGitHubAPIErrors(middlewareCtx)
139+
require.NoError(t, err)
140+
assert.Len(t, apiErrors, 2, "Middleware should see errors added after it captured the context")
141+
142+
assert.Equal(t, "business logic failed", apiErrors[0].Message)
143+
assert.Equal(t, "second failure", apiErrors[1].Message)
144+
})
145+
146+
t.Run("context without GitHub errors returns error", func(t *testing.T) {
147+
// Given a regular context without GitHub error tracking
148+
ctx := context.Background()
149+
150+
// When we try to retrieve errors
151+
apiErrors, err := GetGitHubAPIErrors(ctx)
152+
153+
// Then it should return an error
154+
assert.Error(t, err)
155+
assert.Contains(t, err.Error(), "context does not contain GitHubCtxErrors")
156+
assert.Nil(t, apiErrors)
157+
158+
// Same for GraphQL errors
159+
gqlErrors, err := GetGitHubGraphQLErrors(ctx)
160+
assert.Error(t, err)
161+
assert.Contains(t, err.Error(), "context does not contain GitHubCtxErrors")
162+
assert.Nil(t, gqlErrors)
163+
})
164+
165+
t.Run("ContextWithGitHubErrors resets existing errors", func(t *testing.T) {
166+
// Given a context with existing errors
167+
ctx := ContextWithGitHubErrors(context.Background())
168+
resp := &github.Response{Response: &http.Response{StatusCode: 404}}
169+
ctx, err := NewGitHubAPIErrorToCtx(ctx, "existing error", resp, fmt.Errorf("error"))
170+
require.NoError(t, err)
171+
172+
// Verify error exists
173+
apiErrors, err := GetGitHubAPIErrors(ctx)
174+
require.NoError(t, err)
175+
assert.Len(t, apiErrors, 1)
176+
177+
// When we call ContextWithGitHubErrors again
178+
resetCtx := ContextWithGitHubErrors(ctx)
179+
180+
// Then the errors should be cleared
181+
apiErrors, err = GetGitHubAPIErrors(resetCtx)
182+
require.NoError(t, err)
183+
assert.Len(t, apiErrors, 0, "Errors should be reset")
184+
})
185+
186+
t.Run("NewGitHubAPIErrorResponse creates MCP error result and stores context error", func(t *testing.T) {
187+
// Given a context with GitHub error tracking enabled
188+
ctx := ContextWithGitHubErrors(context.Background())
189+
190+
resp := &github.Response{Response: &http.Response{StatusCode: 422}}
191+
originalErr := fmt.Errorf("validation failed")
192+
193+
// When we create an API error response
194+
result := NewGitHubAPIErrorResponse(ctx, "API call failed", resp, originalErr)
195+
196+
// Then it should return an MCP error result
197+
require.NotNil(t, result)
198+
assert.True(t, result.IsError)
199+
200+
// And the error should be stored in the context
201+
apiErrors, err := GetGitHubAPIErrors(ctx)
202+
require.NoError(t, err)
203+
require.Len(t, apiErrors, 1)
204+
205+
apiError := apiErrors[0]
206+
assert.Equal(t, "API call failed", apiError.Message)
207+
assert.Equal(t, resp, apiError.Response)
208+
assert.Equal(t, originalErr, apiError.Err)
209+
})
210+
211+
t.Run("NewGitHubGraphQLErrorResponse creates MCP error result and stores context error", func(t *testing.T) {
212+
// Given a context with GitHub error tracking enabled
213+
ctx := ContextWithGitHubErrors(context.Background())
214+
215+
originalErr := fmt.Errorf("mutation failed")
216+
217+
// When we create a GraphQL error response
218+
result := NewGitHubGraphQLErrorResponse(ctx, "GraphQL call failed", originalErr)
219+
220+
// Then it should return an MCP error result
221+
require.NotNil(t, result)
222+
assert.True(t, result.IsError)
223+
224+
// And the error should be stored in the context
225+
gqlErrors, err := GetGitHubGraphQLErrors(ctx)
226+
require.NoError(t, err)
227+
require.Len(t, gqlErrors, 1)
228+
229+
gqlError := gqlErrors[0]
230+
assert.Equal(t, "GraphQL call failed", gqlError.Message)
231+
assert.Equal(t, originalErr, gqlError.Err)
232+
})
233+
234+
t.Run("NewGitHubAPIErrorToCtx with uninitialized context does not error", func(t *testing.T) {
235+
// Given a regular context without GitHub error tracking initialized
236+
ctx := context.Background()
237+
238+
// Create a mock GitHub response
239+
resp := &github.Response{
240+
Response: &http.Response{
241+
StatusCode: 500,
242+
Status: "500 Internal Server Error",
243+
},
244+
}
245+
originalErr := fmt.Errorf("internal server error")
246+
247+
// When we try to add an API error to an uninitialized context
248+
updatedCtx, err := NewGitHubAPIErrorToCtx(ctx, "failed operation", resp, originalErr)
249+
250+
// Then it should not return an error (graceful handling)
251+
assert.NoError(t, err, "NewGitHubAPIErrorToCtx should handle uninitialized context gracefully")
252+
assert.Equal(t, ctx, updatedCtx, "Context should be returned unchanged when not initialized")
253+
254+
// And attempting to retrieve errors should still return an error since context wasn't initialized
255+
apiErrors, err := GetGitHubAPIErrors(updatedCtx)
256+
assert.Error(t, err)
257+
assert.Contains(t, err.Error(), "context does not contain GitHubCtxErrors")
258+
assert.Nil(t, apiErrors)
259+
})
260+
261+
t.Run("NewGitHubAPIErrorToCtx with nil context does not error", func(t *testing.T) {
262+
// Given a nil context
263+
var ctx context.Context = nil
264+
265+
// Create a mock GitHub response
266+
resp := &github.Response{
267+
Response: &http.Response{
268+
StatusCode: 400,
269+
Status: "400 Bad Request",
270+
},
271+
}
272+
originalErr := fmt.Errorf("bad request")
273+
274+
// When we try to add an API error to a nil context
275+
updatedCtx, err := NewGitHubAPIErrorToCtx(ctx, "failed with nil context", resp, originalErr)
276+
277+
// Then it should not return an error (graceful handling)
278+
assert.NoError(t, err, "NewGitHubAPIErrorToCtx should handle nil context gracefully")
279+
assert.Nil(t, updatedCtx, "Context should remain nil when passed as nil")
280+
})
281+
}
282+
283+
func TestGitHubErrorTypes(t *testing.T) {
284+
t.Run("GitHubAPIError implements error interface", func(t *testing.T) {
285+
resp := &github.Response{Response: &http.Response{StatusCode: 404}}
286+
originalErr := fmt.Errorf("not found")
287+
288+
apiErr := newGitHubAPIError("test message", resp, originalErr)
289+
290+
// Should implement error interface
291+
var err error = apiErr
292+
assert.Equal(t, "test message: not found", err.Error())
293+
})
294+
295+
t.Run("GitHubGraphQLError implements error interface", func(t *testing.T) {
296+
originalErr := fmt.Errorf("query failed")
297+
298+
gqlErr := newGitHubGraphQLError("test message", originalErr)
299+
300+
// Should implement error interface
301+
var err error = gqlErr
302+
assert.Equal(t, "test message: query failed", err.Error())
303+
})
304+
}
305+
306+
// TestMiddlewareScenario demonstrates a realistic middleware scenario
307+
func TestMiddlewareScenario(t *testing.T) {
308+
t.Run("realistic middleware error collection scenario", func(t *testing.T) {
309+
// Simulate a realistic HTTP middleware scenario
310+
311+
// 1. Request comes in, middleware sets up error tracking
312+
ctx := ContextWithGitHubErrors(context.Background())
313+
314+
// 2. Middleware stores reference to context for later inspection
315+
var middlewareCtx context.Context
316+
setupMiddleware := func(ctx context.Context) context.Context {
317+
middlewareCtx = ctx
318+
return ctx
319+
}
320+
321+
// 3. Setup middleware
322+
ctx = setupMiddleware(ctx)
323+
324+
// 4. Simulate multiple service calls that add errors
325+
simulateServiceCall1 := func(ctx context.Context) {
326+
resp := &github.Response{Response: &http.Response{StatusCode: 403}}
327+
_, err := NewGitHubAPIErrorToCtx(ctx, "insufficient permissions", resp, fmt.Errorf("forbidden"))
328+
require.NoError(t, err)
329+
}
330+
331+
simulateServiceCall2 := func(ctx context.Context) {
332+
resp := &github.Response{Response: &http.Response{StatusCode: 404}}
333+
_, err := NewGitHubAPIErrorToCtx(ctx, "resource not found", resp, fmt.Errorf("not found"))
334+
require.NoError(t, err)
335+
}
336+
337+
simulateGraphQLCall := func(ctx context.Context) {
338+
gqlErr := newGitHubGraphQLError("mutation failed", fmt.Errorf("invalid input"))
339+
_, err := addGitHubGraphQLErrorToContext(ctx, gqlErr)
340+
require.NoError(t, err)
341+
}
342+
343+
// 5. Execute service calls (without context propagation)
344+
simulateServiceCall1(ctx)
345+
simulateServiceCall2(ctx)
346+
simulateGraphQLCall(ctx)
347+
348+
// 6. Middleware inspects errors at the end of request processing
349+
finalizeMiddleware := func(ctx context.Context) ([]string, []string) {
350+
var apiErrorMessages []string
351+
var gqlErrorMessages []string
352+
353+
if apiErrors, err := GetGitHubAPIErrors(ctx); err == nil {
354+
for _, apiErr := range apiErrors {
355+
apiErrorMessages = append(apiErrorMessages, apiErr.Message)
356+
}
357+
}
358+
359+
if gqlErrors, err := GetGitHubGraphQLErrors(ctx); err == nil {
360+
for _, gqlErr := range gqlErrors {
361+
gqlErrorMessages = append(gqlErrorMessages, gqlErr.Message)
362+
}
363+
}
364+
365+
return apiErrorMessages, gqlErrorMessages
366+
}
367+
368+
// 7. Middleware can see all errors that were added during request processing
369+
apiMessages, gqlMessages := finalizeMiddleware(middlewareCtx)
370+
371+
// Verify all errors were captured
372+
assert.Len(t, apiMessages, 2)
373+
assert.Contains(t, apiMessages, "insufficient permissions")
374+
assert.Contains(t, apiMessages, "resource not found")
375+
376+
assert.Len(t, gqlMessages, 1)
377+
assert.Contains(t, gqlMessages, "mutation failed")
378+
})
379+
}

0 commit comments

Comments
 (0)