package mcptests import ( "encoding/json" "fmt" "strings" "testing" "github.com/maximhq/bifrost/core/schemas" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // ============================================================================= // PATH TRAVERSAL SECURITY TESTS // ============================================================================= func TestReadToolFile_PathTraversalAttacks(t *testing.T) { t.Parallel() config := GetTestConfig(t) if config.HTTPServerURL == "" { t.Skip("MCP_HTTP_URL not set") } // Setup code mode client codeModeClient := GetSampleCodeModeClientConfig(t, config.HTTPServerURL) manager := setupMCPManager(t, codeModeClient) bifrost := setupBifrost(t) bifrost.SetMCPManager(manager) ctx := createTestContext() pathTraversalTests := []struct { name string fileName string expectError bool errorMessage string }{ { name: "basic_path_traversal_parent", fileName: "../../../etc/passwd.pyi", expectError: true, errorMessage: "No server found matching", }, { name: "path_traversal_in_server_name", fileName: "servers/../../secrets.pyi", expectError: true, errorMessage: "No server found matching", }, { name: "double_dot_in_tool_name", fileName: "servers/validserver/../../../etc.pyi", expectError: true, errorMessage: "No server found matching", }, { name: "encoded_path_traversal", fileName: "servers/..%2F..%2F..%2Fetc%2Fpasswd.pyi", expectError: true, errorMessage: "No server found matching", }, { name: "path_with_multiple_slashes", fileName: "servers///..//..//etc//passwd.pyi", expectError: true, errorMessage: "No server found matching", }, { name: "absolute_path", fileName: "/etc/passwd.pyi", expectError: true, errorMessage: "No server found matching", }, } for _, tc := range pathTraversalTests { t.Run(tc.name, func(t *testing.T) { toolCall := schemas.ChatAssistantMessageToolCall{ ID: schemas.Ptr("call-" + tc.name), Type: schemas.Ptr("function"), Function: schemas.ChatAssistantMessageToolCallFunction{ Name: schemas.Ptr("readToolFile"), Arguments: `{"fileName": "` + tc.fileName + `"}`, }, } result, bifrostErr := bifrost.ExecuteChatMCPTool(ctx, &toolCall) if tc.expectError { // Should either return error or error message in result if bifrostErr != nil { assert.Contains(t, bifrostErr.Error.Message, tc.errorMessage) } else if result != nil && result.Content != nil && result.Content.ContentStr != nil { assert.Contains(t, *result.Content.ContentStr, tc.errorMessage) } else { t.Errorf("Expected error but got success") } } }) } } func TestReadToolFile_InvalidToolNames(t *testing.T) { t.Parallel() config := GetTestConfig(t) if config.HTTPServerURL == "" { t.Skip("MCP_HTTP_URL not set") } codeModeClient := GetSampleCodeModeClientConfig(t, config.HTTPServerURL) manager := setupMCPManager(t, codeModeClient) bifrost := setupBifrost(t) bifrost.SetMCPManager(manager) ctx := createTestContext() invalidNameTests := []struct { name string fileName string }{ {"slash_in_tool_name", "servers/validserver/tool/with/slashes.pyi"}, {"dot_dot_in_tool_name", "servers/validserver/tool..name.pyi"}, {"special_chars", "servers/validserver/tool<>:\"|?*.pyi"}, {"null_byte", "servers/validserver/tool\x00name.pyi"}, {"backslash", "servers/validserver/tool\\name.pyi"}, } for _, tc := range invalidNameTests { t.Run(tc.name, func(t *testing.T) { argsMap := map[string]string{"fileName": tc.fileName} argsJSON, err := json.Marshal(argsMap) if err != nil { t.Fatalf("failed to marshal arguments: %v", err) } toolCall := schemas.ChatAssistantMessageToolCall{ ID: schemas.Ptr("call-" + tc.name), Type: schemas.Ptr("function"), Function: schemas.ChatAssistantMessageToolCallFunction{ Name: schemas.Ptr("readToolFile"), Arguments: string(argsJSON), }, } result, bifrostErr := bifrost.ExecuteChatMCPTool(ctx, &toolCall) // Should return error or error message if bifrostErr == nil && result != nil && result.Content != nil && result.Content.ContentStr != nil { // Verify error message indicates tool not found assert.Contains(t, *result.Content.ContentStr, "No server found matching") } }) } } // ============================================================================= // CODE INJECTION SECURITY TESTS // ============================================================================= func TestExecuteToolCode_CodeInjectionAttempts(t *testing.T) { t.Parallel() config := GetTestConfig(t) if config.HTTPServerURL == "" { t.Skip("MCP_HTTP_URL not set") } codeModeClient := GetSampleCodeModeClientConfig(t, config.HTTPServerURL) manager := setupMCPManager(t, codeModeClient) bifrost := setupBifrost(t) bifrost.SetMCPManager(manager) ctx := createTestContext() injectionTests := []struct { name string code string shouldFail bool description string }{ { name: "import_os", code: "import os\nresult = os.system('ls')", shouldFail: true, description: "Attempt to import os module", }, { name: "load_module", code: "load('foo.star', 'bar')\nresult = bar()", shouldFail: true, description: "Attempt to load external module", }, { name: "eval_usage", code: "result = 'done'", shouldFail: false, // Simple assignment should succeed description: "Simple code execution", }, { name: "infinite_loop", code: "def loop():\n while True:\n pass\nloop()", shouldFail: true, description: "Infinite loop should timeout", }, { name: "prototype_pollution", code: "result = {'polluted': 'yes'}", shouldFail: false, // Simple dict creation should succeed description: "Dict creation (Starlark has no prototypes)", }, } for _, tc := range injectionTests { t.Run(tc.name, func(t *testing.T) { codeJSON, _ := json.Marshal(tc.code) toolCall := schemas.ChatAssistantMessageToolCall{ ID: schemas.Ptr("call-" + tc.name), Type: schemas.Ptr("function"), Function: schemas.ChatAssistantMessageToolCallFunction{ Name: schemas.Ptr("executeToolCode"), Arguments: `{"code": ` + string(codeJSON) + `}`, }, } result, bifrostErr := bifrost.ExecuteChatMCPTool(ctx, &toolCall) if tc.shouldFail { // Determine if execution actually failed executionFailed := false var failureReason string if bifrostErr != nil { executionFailed = true failureReason = fmt.Sprintf("bifrostErr: %v", bifrostErr) } else if result != nil && result.Content != nil && result.Content.ContentStr != nil { returnValue, hasError, errorMsg := ParseCodeModeResponse(t, *result.Content.ContentStr) if hasError { executionFailed = true failureReason = fmt.Sprintf("ParseCodeModeResponse error: %s", errorMsg) } else if returnValue != nil { if returnObj, ok := returnValue.(map[string]interface{}); ok { if errorField, ok := returnObj["error"]; ok { executionFailed = true failureReason = fmt.Sprintf("return value error field: %v", errorField) } } } } if !executionFailed { t.Errorf("%s (%s): expected execution to fail but it succeeded", tc.name, tc.description) return } t.Logf("%s: execution failed as expected - %s", tc.name, failureReason) } else { // shouldFail == false: assert execution succeeded without errors if bifrostErr != nil { t.Errorf("%s (%s): unexpected bifrostErr: %v", tc.name, tc.description, bifrostErr) return } if result == nil || result.Content == nil || result.Content.ContentStr == nil { t.Errorf("%s (%s): expected result with content but got nil", tc.name, tc.description) return } _, hasError, errorMsg := ParseCodeModeResponse(t, *result.Content.ContentStr) if hasError { t.Errorf("%s (%s): unexpected error in response: %s", tc.name, tc.description, errorMsg) return } t.Logf("%s: execution succeeded as expected", tc.name) } }) } } // ============================================================================= // INPUT VALIDATION SECURITY TESTS // ============================================================================= func TestListToolFiles_InputValidation(t *testing.T) { t.Parallel() config := GetTestConfig(t) if config.HTTPServerURL == "" { t.Skip("MCP_HTTP_URL not set") } codeModeClient := GetSampleCodeModeClientConfig(t, config.HTTPServerURL) manager := setupMCPManager(t, codeModeClient) bifrost := setupBifrost(t) bifrost.SetMCPManager(manager) ctx := createTestContext() // Test listToolFiles with no parameters (should succeed) toolCall := schemas.ChatAssistantMessageToolCall{ ID: schemas.Ptr("call-list"), Type: schemas.Ptr("function"), Function: schemas.ChatAssistantMessageToolCallFunction{ Name: schemas.Ptr("listToolFiles"), Arguments: `{}`, }, } result, bifrostErr := bifrost.ExecuteChatMCPTool(ctx, &toolCall) require.Nil(t, bifrostErr, "listToolFiles should succeed") require.NotNil(t, result) } func TestReadToolFile_EmptyFileName(t *testing.T) { t.Parallel() config := GetTestConfig(t) if config.HTTPServerURL == "" { t.Skip("MCP_HTTP_URL not set") } codeModeClient := GetSampleCodeModeClientConfig(t, config.HTTPServerURL) manager := setupMCPManager(t, codeModeClient) bifrost := setupBifrost(t) bifrost.SetMCPManager(manager) ctx := createTestContext() testCases := []struct { name string arguments string }{ {"empty_string", `{"fileName": ""}`}, {"only_spaces", `{"fileName": " "}`}, {"missing_field", `{}`}, {"null_value", `{"fileName": null}`}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { toolCall := schemas.ChatAssistantMessageToolCall{ ID: schemas.Ptr("call-" + tc.name), Type: schemas.Ptr("function"), Function: schemas.ChatAssistantMessageToolCallFunction{ Name: schemas.Ptr("readToolFile"), Arguments: tc.arguments, }, } result, bifrostErr := bifrost.ExecuteChatMCPTool(ctx, &toolCall) // Should return error or error message if bifrostErr == nil && result != nil { // Check if error is in result content if result.Content != nil && result.Content.ContentStr != nil { content := *result.Content.ContentStr // Should contain some error indication assert.True(t, strings.Contains(content, "error") || strings.Contains(content, "required") || strings.Contains(content, "invalid") || strings.Contains(content, "found") || // Updated to just "found" not "not found" strings.Contains(content, "Available virtual files"), // Also accept list of available files "Should return error message, got: %s", content) } } }) } } func TestExecuteToolCode_EmptyCodeSecurity(t *testing.T) { t.Parallel() config := GetTestConfig(t) if config.HTTPServerURL == "" { t.Skip("MCP_HTTP_URL not set") } codeModeClient := GetSampleCodeModeClientConfig(t, config.HTTPServerURL) manager := setupMCPManager(t, codeModeClient) bifrost := setupBifrost(t) bifrost.SetMCPManager(manager) ctx := createTestContext() testCases := []struct { name string arguments string }{ {"empty_string", `{"code": ""}`}, {"only_spaces", `{"code": " "}`}, {"only_newlines", `{"code": "\n\n\n"}`}, {"missing_field", `{}`}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { toolCall := schemas.ChatAssistantMessageToolCall{ ID: schemas.Ptr("call-" + tc.name), Type: schemas.Ptr("function"), Function: schemas.ChatAssistantMessageToolCallFunction{ Name: schemas.Ptr("executeToolCode"), Arguments: tc.arguments, }, } result, bifrostErr := bifrost.ExecuteChatMCPTool(ctx, &toolCall) // Should return error if bifrostErr == nil && result != nil && result.Content != nil && result.Content.ContentStr != nil { returnValue, hasError, _ := ParseCodeModeResponse(t, *result.Content.ContentStr) if !hasError { // Check if result is empty or null assert.True(t, returnValue == nil || returnValue == "", "Empty code should return empty or null result") } } }) } } // ============================================================================= // UNICODE AND ENCODING TESTS // ============================================================================= func TestExecuteToolCode_UnicodeInCode(t *testing.T) { t.Parallel() config := GetTestConfig(t) if config.HTTPServerURL == "" { t.Skip("MCP_HTTP_URL not set") } codeModeClient := GetSampleCodeModeClientConfig(t, config.HTTPServerURL) manager := setupMCPManager(t, codeModeClient) bifrost := setupBifrost(t) bifrost.SetMCPManager(manager) ctx := createTestContext() unicodeCode := `result = "Hello 🌍"` codeJSON, _ := json.Marshal(unicodeCode) toolCall := schemas.ChatAssistantMessageToolCall{ ID: schemas.Ptr("call-unicode"), Type: schemas.Ptr("function"), Function: schemas.ChatAssistantMessageToolCallFunction{ Name: schemas.Ptr("executeToolCode"), Arguments: `{"code": ` + string(codeJSON) + `}`, }, } result, bifrostErr := bifrost.ExecuteChatMCPTool(ctx, &toolCall) require.Nil(t, bifrostErr) require.NotNil(t, result) if result.Content != nil && result.Content.ContentStr != nil { returnValue, hasError, errorMsg := ParseCodeModeResponse(t, *result.Content.ContentStr) require.False(t, hasError, "should not have execution error: %s", errorMsg) // Should handle unicode correctly resultStr := fmt.Sprintf("%v", returnValue) assert.Contains(t, resultStr, "Hello 🌍") } } // ============================================================================= // MALFORMED JSON TESTS // ============================================================================= func TestExecuteToolCode_MalformedJSON(t *testing.T) { t.Parallel() config := GetTestConfig(t) if config.HTTPServerURL == "" { t.Skip("MCP_HTTP_URL not set") } codeModeClient := GetSampleCodeModeClientConfig(t, config.HTTPServerURL) manager := setupMCPManager(t, codeModeClient) bifrost := setupBifrost(t) bifrost.SetMCPManager(manager) ctx := createTestContext() malformedTests := []struct { name string arguments string }{ {"missing_closing_brace", `{"code": "return 42"`}, {"missing_quotes", `{code: "return 42"}`}, {"trailing_comma", `{"code": "return 42",}`}, {"unescaped_newline", `{"code": "return 42"}`}, {"invalid_escape", `{"code": "return \x"}`}, } for _, tc := range malformedTests { t.Run(tc.name, func(t *testing.T) { toolCall := schemas.ChatAssistantMessageToolCall{ ID: schemas.Ptr("call-" + tc.name), Type: schemas.Ptr("function"), Function: schemas.ChatAssistantMessageToolCallFunction{ Name: schemas.Ptr("executeToolCode"), Arguments: tc.arguments, }, } result, bifrostErr := bifrost.ExecuteChatMCPTool(ctx, &toolCall) // Should return error if bifrostErr != nil { assert.NotEmpty(t, bifrostErr.Error.Message) } else if result != nil && result.Content != nil && result.Content.ContentStr != nil { // Error might be in result content content := *result.Content.ContentStr // Should indicate some kind of error assert.True(t, strings.Contains(content, "error") || strings.Contains(content, "invalid") || strings.Contains(content, "failed"), "Should indicate error, got: %s", content) } }) } } // ============================================================================= // LINE NUMBER BOUNDARY TESTS // ============================================================================= func TestReadToolFile_LineNumberBoundaries(t *testing.T) { t.Parallel() config := GetTestConfig(t) if config.HTTPServerURL == "" { t.Skip("MCP_HTTP_URL not set") } codeModeClient := GetSampleCodeModeClientConfig(t, config.HTTPServerURL) manager := setupMCPManager(t, codeModeClient) bifrost := setupBifrost(t) bifrost.SetMCPManager(manager) ctx := createTestContext() // First, list available files to get a real server name listCall := schemas.ChatAssistantMessageToolCall{ ID: schemas.Ptr("call-list"), Type: schemas.Ptr("function"), Function: schemas.ChatAssistantMessageToolCallFunction{ Name: schemas.Ptr("listToolFiles"), Arguments: `{}`, }, } listResult, _ := bifrost.ExecuteChatMCPTool(ctx, &listCall) if listResult == nil || listResult.Content == nil || listResult.Content.ContentStr == nil { t.Skip("No code mode servers available") } // Parse the list result to get a real file name content := *listResult.Content.ContentStr if !strings.Contains(content, ".pyi") { t.Skip("No .pyi files found") } // Extract first .pyi file name lines := strings.Split(content, "\n") var firstFile string for _, line := range lines { if strings.Contains(line, ".pyi") { // Extract just the filename parts := strings.Fields(line) for _, part := range parts { if strings.HasSuffix(part, ".pyi") { firstFile = strings.Trim(part, "[]\"',") break } } if firstFile != "" { break } } } if firstFile == "" { t.Skip("Could not extract file name from list") } // The readToolFile tool handles invalid line numbers gracefully by returning content // rather than returning explicit error messages. Test that the tool doesn't crash // and returns some content for edge cases. boundaryTests := []struct { name string startLine int endLine int description string }{ { name: "start_line_zero", startLine: 0, endLine: 5, description: "Tool should handle startLine=0 gracefully", }, { name: "start_line_negative", startLine: -1, endLine: 5, description: "Tool should handle negative startLine gracefully", }, { name: "end_line_before_start", startLine: 10, endLine: 5, description: "Tool should handle endLine < startLine gracefully", }, { name: "very_large_line_number", startLine: 1, endLine: 999999, description: "Tool should handle very large endLine gracefully", }, } for _, tc := range boundaryTests { t.Run(tc.name, func(t *testing.T) { args := map[string]interface{}{ "fileName": firstFile, "startLine": tc.startLine, "endLine": tc.endLine, } argsJSON, _ := json.Marshal(args) toolCall := schemas.ChatAssistantMessageToolCall{ ID: schemas.Ptr("call-" + tc.name), Type: schemas.Ptr("function"), Function: schemas.ChatAssistantMessageToolCallFunction{ Name: schemas.Ptr("readToolFile"), Arguments: string(argsJSON), }, } result, bifrostErr := bifrost.ExecuteChatMCPTool(ctx, &toolCall) // The tool should not crash and should return a result // It handles invalid line numbers gracefully by returning content if bifrostErr != nil { t.Logf("%s: Got bifrost error (acceptable): %v", tc.description, bifrostErr) } else { require.NotNil(t, result, "%s: result should not be nil", tc.description) require.NotNil(t, result.Content, "%s: result.Content should not be nil", tc.description) require.NotNil(t, result.Content.ContentStr, "%s: result.Content.ContentStr should not be nil", tc.description) // Just verify we got some content back (tool handles gracefully) t.Logf("%s: Got response with %d characters", tc.description, len(*result.Content.ContentStr)) } }) } }