Spaces:
Build error
Build error
| package errors | |
| import ( | |
| "context" | |
| "fmt" | |
| "net/http" | |
| "testing" | |
| "github.com/google/go-github/v74/github" | |
| "github.com/stretchr/testify/assert" | |
| "github.com/stretchr/testify/require" | |
| ) | |
| func TestGitHubErrorContext(t *testing.T) { | |
| t.Run("API errors can be added to context and retrieved", func(t *testing.T) { | |
| // Given a context with GitHub error tracking enabled | |
| ctx := ContextWithGitHubErrors(context.Background()) | |
| // Create a mock GitHub response | |
| resp := &github.Response{ | |
| Response: &http.Response{ | |
| StatusCode: 404, | |
| Status: "404 Not Found", | |
| }, | |
| } | |
| originalErr := fmt.Errorf("resource not found") | |
| // When we add an API error to the context | |
| updatedCtx, err := NewGitHubAPIErrorToCtx(ctx, "failed to fetch resource", resp, originalErr) | |
| require.NoError(t, err) | |
| // Then we should be able to retrieve the error from the updated context | |
| apiErrors, err := GetGitHubAPIErrors(updatedCtx) | |
| require.NoError(t, err) | |
| require.Len(t, apiErrors, 1) | |
| apiError := apiErrors[0] | |
| assert.Equal(t, "failed to fetch resource", apiError.Message) | |
| assert.Equal(t, resp, apiError.Response) | |
| assert.Equal(t, originalErr, apiError.Err) | |
| assert.Equal(t, "failed to fetch resource: resource not found", apiError.Error()) | |
| }) | |
| t.Run("GraphQL errors can be added to context and retrieved", func(t *testing.T) { | |
| // Given a context with GitHub error tracking enabled | |
| ctx := ContextWithGitHubErrors(context.Background()) | |
| originalErr := fmt.Errorf("GraphQL query failed") | |
| // When we add a GraphQL error to the context | |
| graphQLErr := newGitHubGraphQLError("failed to execute mutation", originalErr) | |
| updatedCtx, err := addGitHubGraphQLErrorToContext(ctx, graphQLErr) | |
| require.NoError(t, err) | |
| // Then we should be able to retrieve the error from the updated context | |
| gqlErrors, err := GetGitHubGraphQLErrors(updatedCtx) | |
| require.NoError(t, err) | |
| require.Len(t, gqlErrors, 1) | |
| gqlError := gqlErrors[0] | |
| assert.Equal(t, "failed to execute mutation", gqlError.Message) | |
| assert.Equal(t, originalErr, gqlError.Err) | |
| assert.Equal(t, "failed to execute mutation: GraphQL query failed", gqlError.Error()) | |
| }) | |
| t.Run("multiple errors can be accumulated in context", func(t *testing.T) { | |
| // Given a context with GitHub error tracking enabled | |
| ctx := ContextWithGitHubErrors(context.Background()) | |
| // When we add multiple API errors | |
| resp1 := &github.Response{Response: &http.Response{StatusCode: 404}} | |
| resp2 := &github.Response{Response: &http.Response{StatusCode: 403}} | |
| ctx, err := NewGitHubAPIErrorToCtx(ctx, "first error", resp1, fmt.Errorf("not found")) | |
| require.NoError(t, err) | |
| ctx, err = NewGitHubAPIErrorToCtx(ctx, "second error", resp2, fmt.Errorf("forbidden")) | |
| require.NoError(t, err) | |
| // And add a GraphQL error | |
| gqlErr := newGitHubGraphQLError("graphql error", fmt.Errorf("query failed")) | |
| ctx, err = addGitHubGraphQLErrorToContext(ctx, gqlErr) | |
| require.NoError(t, err) | |
| // Then we should be able to retrieve all errors | |
| apiErrors, err := GetGitHubAPIErrors(ctx) | |
| require.NoError(t, err) | |
| assert.Len(t, apiErrors, 2) | |
| gqlErrors, err := GetGitHubGraphQLErrors(ctx) | |
| require.NoError(t, err) | |
| assert.Len(t, gqlErrors, 1) | |
| // Verify error details | |
| assert.Equal(t, "first error", apiErrors[0].Message) | |
| assert.Equal(t, "second error", apiErrors[1].Message) | |
| assert.Equal(t, "graphql error", gqlErrors[0].Message) | |
| }) | |
| t.Run("context pointer sharing allows middleware to inspect errors without context propagation", func(t *testing.T) { | |
| // This test demonstrates the key behavior: even when the context itself | |
| // isn't propagated through function calls, the pointer to the error slice | |
| // is shared, allowing middleware to inspect errors that were added later. | |
| // Given a context with GitHub error tracking enabled | |
| originalCtx := ContextWithGitHubErrors(context.Background()) | |
| // Simulate a middleware that captures the context early | |
| var middlewareCtx context.Context | |
| // Middleware function that captures the context | |
| middleware := func(ctx context.Context) { | |
| middlewareCtx = ctx // Middleware saves the context reference | |
| } | |
| // Call middleware with the original context | |
| middleware(originalCtx) | |
| // Simulate some business logic that adds errors to the context | |
| // but doesn't propagate the updated context back to middleware | |
| businessLogic := func(ctx context.Context) { | |
| resp := &github.Response{Response: &http.Response{StatusCode: 500}} | |
| // Add an error to the context (this modifies the shared pointer) | |
| _, err := NewGitHubAPIErrorToCtx(ctx, "business logic failed", resp, fmt.Errorf("internal error")) | |
| require.NoError(t, err) | |
| // Add another error | |
| _, err = NewGitHubAPIErrorToCtx(ctx, "second failure", resp, fmt.Errorf("another error")) | |
| require.NoError(t, err) | |
| } | |
| // Execute business logic - note that we don't propagate the returned context | |
| businessLogic(originalCtx) | |
| // Then the middleware should be able to see the errors that were added | |
| // even though it only has a reference to the original context | |
| apiErrors, err := GetGitHubAPIErrors(middlewareCtx) | |
| require.NoError(t, err) | |
| assert.Len(t, apiErrors, 2, "Middleware should see errors added after it captured the context") | |
| assert.Equal(t, "business logic failed", apiErrors[0].Message) | |
| assert.Equal(t, "second failure", apiErrors[1].Message) | |
| }) | |
| t.Run("context without GitHub errors returns error", func(t *testing.T) { | |
| // Given a regular context without GitHub error tracking | |
| ctx := context.Background() | |
| // When we try to retrieve errors | |
| apiErrors, err := GetGitHubAPIErrors(ctx) | |
| // Then it should return an error | |
| assert.Error(t, err) | |
| assert.Contains(t, err.Error(), "context does not contain GitHubCtxErrors") | |
| assert.Nil(t, apiErrors) | |
| // Same for GraphQL errors | |
| gqlErrors, err := GetGitHubGraphQLErrors(ctx) | |
| assert.Error(t, err) | |
| assert.Contains(t, err.Error(), "context does not contain GitHubCtxErrors") | |
| assert.Nil(t, gqlErrors) | |
| }) | |
| t.Run("ContextWithGitHubErrors resets existing errors", func(t *testing.T) { | |
| // Given a context with existing errors | |
| ctx := ContextWithGitHubErrors(context.Background()) | |
| resp := &github.Response{Response: &http.Response{StatusCode: 404}} | |
| ctx, err := NewGitHubAPIErrorToCtx(ctx, "existing error", resp, fmt.Errorf("error")) | |
| require.NoError(t, err) | |
| // Verify error exists | |
| apiErrors, err := GetGitHubAPIErrors(ctx) | |
| require.NoError(t, err) | |
| assert.Len(t, apiErrors, 1) | |
| // When we call ContextWithGitHubErrors again | |
| resetCtx := ContextWithGitHubErrors(ctx) | |
| // Then the errors should be cleared | |
| apiErrors, err = GetGitHubAPIErrors(resetCtx) | |
| require.NoError(t, err) | |
| assert.Len(t, apiErrors, 0, "Errors should be reset") | |
| }) | |
| t.Run("NewGitHubAPIErrorResponse creates MCP error result and stores context error", func(t *testing.T) { | |
| // Given a context with GitHub error tracking enabled | |
| ctx := ContextWithGitHubErrors(context.Background()) | |
| resp := &github.Response{Response: &http.Response{StatusCode: 422}} | |
| originalErr := fmt.Errorf("validation failed") | |
| // When we create an API error response | |
| result := NewGitHubAPIErrorResponse(ctx, "API call failed", resp, originalErr) | |
| // Then it should return an MCP error result | |
| require.NotNil(t, result) | |
| assert.True(t, result.IsError) | |
| // And the error should be stored in the context | |
| apiErrors, err := GetGitHubAPIErrors(ctx) | |
| require.NoError(t, err) | |
| require.Len(t, apiErrors, 1) | |
| apiError := apiErrors[0] | |
| assert.Equal(t, "API call failed", apiError.Message) | |
| assert.Equal(t, resp, apiError.Response) | |
| assert.Equal(t, originalErr, apiError.Err) | |
| }) | |
| t.Run("NewGitHubGraphQLErrorResponse creates MCP error result and stores context error", func(t *testing.T) { | |
| // Given a context with GitHub error tracking enabled | |
| ctx := ContextWithGitHubErrors(context.Background()) | |
| originalErr := fmt.Errorf("mutation failed") | |
| // When we create a GraphQL error response | |
| result := NewGitHubGraphQLErrorResponse(ctx, "GraphQL call failed", originalErr) | |
| // Then it should return an MCP error result | |
| require.NotNil(t, result) | |
| assert.True(t, result.IsError) | |
| // And the error should be stored in the context | |
| gqlErrors, err := GetGitHubGraphQLErrors(ctx) | |
| require.NoError(t, err) | |
| require.Len(t, gqlErrors, 1) | |
| gqlError := gqlErrors[0] | |
| assert.Equal(t, "GraphQL call failed", gqlError.Message) | |
| assert.Equal(t, originalErr, gqlError.Err) | |
| }) | |
| t.Run("NewGitHubAPIErrorToCtx with uninitialized context does not error", func(t *testing.T) { | |
| // Given a regular context without GitHub error tracking initialized | |
| ctx := context.Background() | |
| // Create a mock GitHub response | |
| resp := &github.Response{ | |
| Response: &http.Response{ | |
| StatusCode: 500, | |
| Status: "500 Internal Server Error", | |
| }, | |
| } | |
| originalErr := fmt.Errorf("internal server error") | |
| // When we try to add an API error to an uninitialized context | |
| updatedCtx, err := NewGitHubAPIErrorToCtx(ctx, "failed operation", resp, originalErr) | |
| // Then it should not return an error (graceful handling) | |
| assert.NoError(t, err, "NewGitHubAPIErrorToCtx should handle uninitialized context gracefully") | |
| assert.Equal(t, ctx, updatedCtx, "Context should be returned unchanged when not initialized") | |
| // And attempting to retrieve errors should still return an error since context wasn't initialized | |
| apiErrors, err := GetGitHubAPIErrors(updatedCtx) | |
| assert.Error(t, err) | |
| assert.Contains(t, err.Error(), "context does not contain GitHubCtxErrors") | |
| assert.Nil(t, apiErrors) | |
| }) | |
| t.Run("NewGitHubAPIErrorToCtx with nil context does not error", func(t *testing.T) { | |
| // Given a nil context | |
| var ctx context.Context | |
| // Create a mock GitHub response | |
| resp := &github.Response{ | |
| Response: &http.Response{ | |
| StatusCode: 400, | |
| Status: "400 Bad Request", | |
| }, | |
| } | |
| originalErr := fmt.Errorf("bad request") | |
| // When we try to add an API error to a nil context | |
| updatedCtx, err := NewGitHubAPIErrorToCtx(ctx, "failed with nil context", resp, originalErr) | |
| // Then it should not return an error (graceful handling) | |
| assert.NoError(t, err, "NewGitHubAPIErrorToCtx should handle nil context gracefully") | |
| assert.Nil(t, updatedCtx, "Context should remain nil when passed as nil") | |
| }) | |
| } | |
| func TestGitHubErrorTypes(t *testing.T) { | |
| t.Run("GitHubAPIError implements error interface", func(t *testing.T) { | |
| resp := &github.Response{Response: &http.Response{StatusCode: 404}} | |
| originalErr := fmt.Errorf("not found") | |
| apiErr := newGitHubAPIError("test message", resp, originalErr) | |
| // Should implement error interface | |
| var err error = apiErr | |
| assert.Equal(t, "test message: not found", err.Error()) | |
| }) | |
| t.Run("GitHubGraphQLError implements error interface", func(t *testing.T) { | |
| originalErr := fmt.Errorf("query failed") | |
| gqlErr := newGitHubGraphQLError("test message", originalErr) | |
| // Should implement error interface | |
| var err error = gqlErr | |
| assert.Equal(t, "test message: query failed", err.Error()) | |
| }) | |
| } | |
| // TestMiddlewareScenario demonstrates a realistic middleware scenario | |
| func TestMiddlewareScenario(t *testing.T) { | |
| t.Run("realistic middleware error collection scenario", func(t *testing.T) { | |
| // Simulate a realistic HTTP middleware scenario | |
| // 1. Request comes in, middleware sets up error tracking | |
| ctx := ContextWithGitHubErrors(context.Background()) | |
| // 2. Middleware stores reference to context for later inspection | |
| var middlewareCtx context.Context | |
| setupMiddleware := func(ctx context.Context) context.Context { | |
| middlewareCtx = ctx | |
| return ctx | |
| } | |
| // 3. Setup middleware | |
| ctx = setupMiddleware(ctx) | |
| // 4. Simulate multiple service calls that add errors | |
| simulateServiceCall1 := func(ctx context.Context) { | |
| resp := &github.Response{Response: &http.Response{StatusCode: 403}} | |
| _, err := NewGitHubAPIErrorToCtx(ctx, "insufficient permissions", resp, fmt.Errorf("forbidden")) | |
| require.NoError(t, err) | |
| } | |
| simulateServiceCall2 := func(ctx context.Context) { | |
| resp := &github.Response{Response: &http.Response{StatusCode: 404}} | |
| _, err := NewGitHubAPIErrorToCtx(ctx, "resource not found", resp, fmt.Errorf("not found")) | |
| require.NoError(t, err) | |
| } | |
| simulateGraphQLCall := func(ctx context.Context) { | |
| gqlErr := newGitHubGraphQLError("mutation failed", fmt.Errorf("invalid input")) | |
| _, err := addGitHubGraphQLErrorToContext(ctx, gqlErr) | |
| require.NoError(t, err) | |
| } | |
| // 5. Execute service calls (without context propagation) | |
| simulateServiceCall1(ctx) | |
| simulateServiceCall2(ctx) | |
| simulateGraphQLCall(ctx) | |
| // 6. Middleware inspects errors at the end of request processing | |
| finalizeMiddleware := func(ctx context.Context) ([]string, []string) { | |
| var apiErrorMessages []string | |
| var gqlErrorMessages []string | |
| if apiErrors, err := GetGitHubAPIErrors(ctx); err == nil { | |
| for _, apiErr := range apiErrors { | |
| apiErrorMessages = append(apiErrorMessages, apiErr.Message) | |
| } | |
| } | |
| if gqlErrors, err := GetGitHubGraphQLErrors(ctx); err == nil { | |
| for _, gqlErr := range gqlErrors { | |
| gqlErrorMessages = append(gqlErrorMessages, gqlErr.Message) | |
| } | |
| } | |
| return apiErrorMessages, gqlErrorMessages | |
| } | |
| // 7. Middleware can see all errors that were added during request processing | |
| apiMessages, gqlMessages := finalizeMiddleware(middlewareCtx) | |
| // Verify all errors were captured | |
| assert.Len(t, apiMessages, 2) | |
| assert.Contains(t, apiMessages, "insufficient permissions") | |
| assert.Contains(t, apiMessages, "resource not found") | |
| assert.Len(t, gqlMessages, 1) | |
| assert.Contains(t, gqlMessages, "mutation failed") | |
| }) | |
| } | |