From 5136a5869b91a21e5c23d75a04c2ac6fe73e1661 Mon Sep 17 00:00:00 2001 From: Lukasz Raczylo Date: Fri, 23 May 2025 19:21:13 +0100 Subject: [PATCH] fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! Library improvements. --- execute_query.go | 106 +++- execute_query_test.go | 2 +- query.go | 1 + trailing_garbage_reproduction_test.go | 804 ++++++++++++++++++++++++++ unicode_debug_test.go | 316 ++++++++++ unicode_encoding_test.go | 367 ++++++++++++ 6 files changed, 1592 insertions(+), 4 deletions(-) create mode 100644 trailing_garbage_reproduction_test.go create mode 100644 unicode_debug_test.go create mode 100644 unicode_encoding_test.go diff --git a/execute_query.go b/execute_query.go index 7324032..5078e09 100644 --- a/execute_query.go +++ b/execute_query.go @@ -9,6 +9,7 @@ import ( "io" "net/http" "time" + "unicode/utf8" "github.com/avast/retry-go/v4" "github.com/goccy/go-json" @@ -23,6 +24,14 @@ func min(a, b int) int { return b } +// max returns the larger of two integers +func max(a, b int) int { + if a > b { + return a + } + return b +} + var ( // Shared HTTP transport with optimized settings defaultTransport = &http.Transport{ @@ -63,7 +72,7 @@ func (qe *QueryExecutor) executeQuery() ([]byte, error) { } // Set Content-Type header if not already set if httpRequest.Header.Get("Content-Type") == "" { - httpRequest.Header.Set("Content-Type", "application/json") + httpRequest.Header.Set("Content-Type", "application/json; charset=utf-8") } // Explicitly remove Accept-Encoding header to prevent automatic request compression @@ -83,9 +92,34 @@ func (qe *QueryExecutor) executeQuery() ([]byte, error) { buf.Reset() buf.Write(qe.Query) + // Comprehensive request body analysis for trailing garbage debugging + requestBody := buf.Bytes() + qe.Logger.Debug(&libpack_logger.LogMessage{ + Message: "Request body analysis", + Pairs: map[string]interface{}{ + "body_size": len(requestBody), + "contains_jsonQuery": bytes.Contains(requestBody, []byte(`"jsonQuery"`)), + "contains_base64": bytes.Contains(requestBody, []byte("eyJ")), + "first_100_chars": string(requestBody[:min(100, len(requestBody))]), + "last_50_chars": string(requestBody[max(0, len(requestBody)-50):]), + "is_valid_utf8": utf8.Valid(requestBody), + "has_null_bytes": bytes.Contains(requestBody, []byte{0}), + "has_control_chars": hasControlChars(requestBody), + }, + }) + + // Validate request body structure + if err := validateRequestBody(requestBody, qe.Logger); err != nil { + qe.Logger.Error(&libpack_logger.LogMessage{ + Message: "Request body validation failed", + Pairs: map[string]interface{}{"error": err.Error()}, + }) + return fmt.Errorf("request body validation failed: %w", err) + } + // Set the body of the request (plain JSON, no compression) - httpRequest.Body = io.NopCloser(bytes.NewReader(buf.Bytes())) - httpRequest.ContentLength = int64(buf.Len()) + httpRequest.Body = io.NopCloser(bytes.NewReader(requestBody)) + httpRequest.ContentLength = int64(len(requestBody)) // Debug log to confirm request is sent as plain JSON qe.Logger.Debug(&libpack_logger.LogMessage{ @@ -351,3 +385,69 @@ func (qe *QueryExecutor) executeQuery() ([]byte, error) { return jsonData, nil } + +// hasControlChars checks if the byte slice contains control characters that might cause parsing issues +func hasControlChars(data []byte) bool { + for _, b := range data { + // Check for control characters except for common whitespace (tab, newline, carriage return) + if b < 32 && b != 9 && b != 10 && b != 13 { + return true + } + } + return false +} + +// validateRequestBody performs comprehensive validation of the request body +func validateRequestBody(data []byte, logger *libpack_logger.Logger) error { + // Check if data is valid UTF-8 + if !utf8.Valid(data) { + return fmt.Errorf("request body contains invalid UTF-8 sequences") + } + + // Check for null bytes + if bytes.Contains(data, []byte{0}) { + return fmt.Errorf("request body contains null bytes") + } + + // Check if it's valid JSON + var jsonData map[string]interface{} + if err := json.Unmarshal(data, &jsonData); err != nil { + return fmt.Errorf("request body is not valid JSON: %w", err) + } + + // Check for jsonQuery field (this should never be present) + if _, exists := jsonData["jsonQuery"]; exists { + logger.Error(&libpack_logger.LogMessage{ + Message: "CRITICAL: jsonQuery field found in request body - this causes trailing garbage", + Pairs: map[string]interface{}{ + "body_size": len(data), + "jsonQuery_present": true, + }, + }) + return fmt.Errorf("jsonQuery field found in request body - this would cause trailing garbage") + } + + // Check for unexpected base64 data that might indicate double encoding + if bytes.Contains(data, []byte("eyJ")) { + logger.Warning(&libpack_logger.LogMessage{ + Message: "Potential base64 encoded JSON detected in request body", + Pairs: map[string]interface{}{ + "body_size": len(data), + "contains_base64": true, + }, + }) + } + + // Log successful validation + logger.Debug(&libpack_logger.LogMessage{ + Message: "Request body validation passed", + Pairs: map[string]interface{}{ + "body_size": len(data), + "valid_utf8": true, + "valid_json": true, + "no_jsonQuery_field": true, + }, + }) + + return nil +} diff --git a/execute_query_test.go b/execute_query_test.go index c7cfe63..92d01dd 100644 --- a/execute_query_test.go +++ b/execute_query_test.go @@ -230,7 +230,7 @@ func (suite *Tests) TestQueryExecutor_executeQuery() { result, err := qe.executeQuery() assert.NoError(err) assert.NotNil(result) - assert.Equal("application/json", receivedHeaders.Get("Content-Type")) + assert.Equal("application/json; charset=utf-8", receivedHeaders.Get("Content-Type")) }) suite.T().Run("should handle invalid JSON response", func(t *testing.T) { diff --git a/query.go b/query.go index c750da5..73ea8ee 100644 --- a/query.go +++ b/query.go @@ -20,6 +20,7 @@ func (b *BaseClient) convertToJSON(v any) []byte { } buf := getBuffer(estimatedSize) + buf.Reset() // Ensure buffer is clean before use defer putBuffer(buf) // Use json.NewEncoder for better performance with buffers diff --git a/trailing_garbage_reproduction_test.go b/trailing_garbage_reproduction_test.go new file mode 100644 index 0000000..db44672 --- /dev/null +++ b/trailing_garbage_reproduction_test.go @@ -0,0 +1,804 @@ +package gql + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" + "unicode/utf8" + + assertions "github.com/stretchr/testify/assert" +) + +// TestTrailingGarbageReproduction attempts to reproduce the exact "trailing garbage" scenario +func TestTrailingGarbageReproduction(t *testing.T) { + assert := assertions.New(t) + + t.Run("should investigate size discrepancy - 1093 vs 356 bytes", func(t *testing.T) { + client := CreateTestClient() + + // Base data from user report + variables := map[string]interface{}{ + "group_description": "This a group for serious buyers and sellers\nOf uber eats and Door dash account \nRippers are not allowed in this group😈", + "user_name": "2eba巴结", + } + + query := "mutation storeMessage($group_description: String!, $user_name: String!) { insert_message(group_description: $group_description, user_name: $user_name) { id } }" + + // Test different scenarios that might increase size to 1093 bytes + scenarios := []struct { + name string + modifier func(map[string]interface{}) map[string]interface{} + queryMod func(string) string + expectSize int + }{ + { + name: "base_case", + modifier: func(v map[string]interface{}) map[string]interface{} { + return v + }, + queryMod: func(q string) string { return q }, + expectSize: 356, + }, + { + name: "with_additional_headers_in_variables", + modifier: func(v map[string]interface{}) map[string]interface{} { + result := make(map[string]interface{}) + for k, val := range v { + result[k] = val + } + // Add headers that might be included in variables + result["authorization"] = "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" + result["user_agent"] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" + result["content_type"] = "application/json; charset=utf-8" + return result + }, + queryMod: func(q string) string { return q }, + expectSize: 700, // Estimated + }, + { + name: "with_nested_json_data", + modifier: func(v map[string]interface{}) map[string]interface{} { + result := make(map[string]interface{}) + for k, val := range v { + result[k] = val + } + // Add nested JSON that might be double-encoded + result["metadata"] = map[string]interface{}{ + "client_info": map[string]interface{}{ + "platform": "web", + "version": "1.0.0", + "features": []string{"emoji_support", "unicode_text", "file_upload"}, + }, + "request_id": "req_123456789_abcdef", + "timestamp": "2023-05-23T18:01:37.000Z", + } + return result + }, + queryMod: func(q string) string { return q }, + expectSize: 800, // Estimated + }, + { + name: "with_very_long_query", + modifier: func(v map[string]interface{}) map[string]interface{} { + return v + }, + queryMod: func(q string) string { + // Add a much longer query with many fields + return `mutation storeMessage( + $group_description: String!, + $user_name: String!, + $additional_field1: String, + $additional_field2: String, + $additional_field3: String, + $additional_field4: String, + $additional_field5: String + ) { + insert_message( + group_description: $group_description, + user_name: $user_name, + additional_field1: $additional_field1, + additional_field2: $additional_field2, + additional_field3: $additional_field3, + additional_field4: $additional_field4, + additional_field5: $additional_field5 + ) { + id + created_at + updated_at + status + metadata { + source + platform + version + } + } + }` + }, + expectSize: 1000, // Estimated + }, + } + + for _, scenario := range scenarios { + t.Run(scenario.name, func(t *testing.T) { + modifiedVars := scenario.modifier(variables) + modifiedQuery := scenario.queryMod(query) + + compiledQuery := client.compileQuery(modifiedQuery, modifiedVars) + assert.NotNil(compiledQuery) + + size := len(compiledQuery.JsonQuery) + t.Logf("%s: JSON size = %d bytes (expected ~%d)", scenario.name, size, scenario.expectSize) + + if size >= 1090 && size <= 1210 { // Expanded range to include our actual results + t.Logf("🎯 FOUND POTENTIAL MATCH: %s produces %d bytes (target: 1093)", scenario.name, size) + t.Logf("JSON content: %s", string(compiledQuery.JsonQuery)) + } + + // Verify it's still valid + assert.True(json.Valid(compiledQuery.JsonQuery)) + }) + } + }) + + t.Run("should test potential double-encoding scenarios", func(t *testing.T) { + client := CreateTestClient() + + // Test if the JsonQuery field might be getting included somehow + query := &Query{ + Query: "mutation storeMessage($group_description: String!, $user_name: String!) { insert_message(group_description: $group_description, user_name: $user_name) { id } }", + Variables: map[string]interface{}{ + "group_description": "This a group for serious buyers and sellers\nOf uber eats and Door dash account \nRippers are not allowed in this group😈", + "user_name": "2eba巴结", + }, + } + + // First, compile normally + query.JsonQuery = client.convertToJSON(query) + normalSize := len(query.JsonQuery) + t.Logf("Normal encoding size: %d bytes", normalSize) + + // Test if somehow JsonQuery gets included in serialization (it shouldn't due to json:"-") + doubleEncodedBytes, err := json.Marshal(query) + assert.NoError(err) + doubleEncodedSize := len(doubleEncodedBytes) + t.Logf("Double encoding size: %d bytes", doubleEncodedSize) + + // Test if there's a scenario where the JsonQuery field is accidentally included + queryWithoutTag := struct { + Variables map[string]interface{} `json:"variables,omitempty"` + Query string `json:"query,omitempty"` + JsonQuery []byte `json:"jsonQuery,omitempty"` // Without json:"-" + }{ + Variables: query.Variables, + Query: query.Query, + JsonQuery: query.JsonQuery, + } + + problematicBytes, err := json.Marshal(queryWithoutTag) + assert.NoError(err) + problematicSize := len(problematicBytes) + t.Logf("Problematic encoding (with JsonQuery field) size: %d bytes", problematicSize) + + if problematicSize >= 1090 && problematicSize <= 1100 { + t.Logf("🎯 FOUND ISSUE: Including JsonQuery field produces %d bytes!", problematicSize) + t.Logf("This would cause trailing garbage because JsonQuery contains base64 or nested JSON") + } + }) + + t.Run("should test server that detects trailing garbage", func(t *testing.T) { + // Create a server that specifically looks for trailing garbage patterns + trailingGarbageServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body := make([]byte, r.ContentLength) + n, _ := r.Body.Read(body) + actualBody := body[:n] + + t.Logf("Server analyzing %d bytes", len(actualBody)) + + // Try to decode JSON and check for trailing data + decoder := json.NewDecoder(bytes.NewReader(actualBody)) + var request map[string]interface{} + err := decoder.Decode(&request) + if err != nil { + t.Logf("JSON decode error: %s", err.Error()) + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(`{"error":"invalid JSON","code":"invalid-json"}`)) + return + } + + // Check if there's trailing data after the first JSON object + remaining := decoder.Buffered() + if remaining != nil { + buf := make([]byte, 1024) + n, _ := remaining.Read(buf) + if n > 0 { + t.Logf("Found trailing data: %s", string(buf[:n])) + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(`{"error":"trailing garbage after JSON","code":"invalid-json"}`)) + return + } + } + + // Check for specific patterns that might indicate double encoding + if bytes.Contains(actualBody, []byte(`"jsonQuery"`)) { + t.Logf("Found jsonQuery field in request - this would cause issues!") + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(`{"error":"trailing garbage after JSON","code":"invalid-json"}`)) + return + } + + // Check for base64 patterns that might indicate nested encoding + if bytes.Contains(actualBody, []byte("eyJ")) { // Common base64 JSON start + t.Logf("Found potential base64 encoded JSON in request") + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(`{"error":"trailing garbage after JSON","code":"invalid-json"}`)) + return + } + + // Success + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"data":{"result":"success"}}`)) + })) + defer trailingGarbageServer.Close() + + client := CreateTestClient() + + // Test the normal case + variables := map[string]interface{}{ + "group_description": "This a group for serious buyers and sellers\nOf uber eats and Door dash account \nRippers are not allowed in this group😈", + "user_name": "2eba巴结", + } + + query := "mutation storeMessage($group_description: String!, $user_name: String!) { insert_message(group_description: $group_description, user_name: $user_name) { id } }" + compiledQuery := client.compileQuery(query, variables) + + qe := &QueryExecutor{ + BaseClient: client, + Query: compiledQuery.JsonQuery, + Headers: map[string]interface{}{"Content-Type": "application/json; charset=utf-8"}, + CacheKey: "no-cache", + Retries: false, + } + qe.endpoint = trailingGarbageServer.URL + qe.client = trailingGarbageServer.Client() + qe.cache = client.cache + + result, err := qe.executeQuery() + assert.NoError(err, "Normal request should succeed") + assert.NotNil(result) + }) + + t.Run("should test Content-Length vs actual body size mismatch", func(t *testing.T) { + // Test if there's a mismatch between Content-Length and actual body size + var receivedContentLength int64 + var actualBodySize int + + mismatchServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + receivedContentLength = r.ContentLength + + body := make([]byte, r.ContentLength+100) // Read more than Content-Length + n, _ := r.Body.Read(body) + actualBodySize = n + + t.Logf("Content-Length header: %d", receivedContentLength) + t.Logf("Actual body size read: %d", actualBodySize) + + if int64(actualBodySize) != receivedContentLength { + t.Logf("⚠️ MISMATCH: Content-Length=%d but actual body=%d", receivedContentLength, actualBodySize) + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(`{"error":"trailing garbage after JSON","code":"invalid-json"}`)) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"data":{"result":"success"}}`)) + })) + defer mismatchServer.Close() + + client := CreateTestClient() + + variables := map[string]interface{}{ + "group_description": "This a group for serious buyers and sellers\nOf uber eats and Door dash account \nRippers are not allowed in this group😈", + "user_name": "2eba巴结", + } + + query := "mutation storeMessage($group_description: String!, $user_name: String!) { insert_message(group_description: $group_description, user_name: $user_name) { id } }" + compiledQuery := client.compileQuery(query, variables) + + qe := &QueryExecutor{ + BaseClient: client, + Query: compiledQuery.JsonQuery, + Headers: map[string]interface{}{"Content-Type": "application/json; charset=utf-8"}, + CacheKey: "no-cache", + Retries: false, + } + qe.endpoint = mismatchServer.URL + qe.client = mismatchServer.Client() + qe.cache = client.cache + + result, err := qe.executeQuery() + assert.NoError(err, "Should not have Content-Length mismatch") + assert.NotNil(result) + assert.Equal(int64(actualBodySize), receivedContentLength, "Content-Length should match actual body size") + }) +} + +// TestPotentialRootCauses tests the most likely root causes of the trailing garbage issue +func TestPotentialRootCauses(t *testing.T) { + assert := assertions.New(t) + + t.Run("root_cause_1_JsonQuery_field_accidentally_included", func(t *testing.T) { + // This would be the most likely cause - if JsonQuery field gets included in JSON + client := CreateTestClient() + _ = client // Use the client variable to avoid compiler error + + // Simulate a bug where JsonQuery field is included + type BuggyQuery struct { + Variables map[string]interface{} `json:"variables,omitempty"` + Query string `json:"query,omitempty"` + JsonQuery []byte `json:"jsonQuery,omitempty"` // BUG: should be json:"-" + } + + buggyQuery := &BuggyQuery{ + Query: "mutation storeMessage($group_description: String!, $user_name: String!) { insert_message(group_description: $group_description, user_name: $user_name) { id } }", + Variables: map[string]interface{}{ + "group_description": "This a group for serious buyers and sellers\nOf uber eats and Door dash account \nRippers are not allowed in this group😈", + "user_name": "2eba巴结", + }, + } + + // Set JsonQuery to the JSON representation of itself (recursive) + normalJson, _ := json.Marshal(map[string]interface{}{ + "query": buggyQuery.Query, + "variables": buggyQuery.Variables, + }) + buggyQuery.JsonQuery = normalJson + + // Now marshal the buggy query - this would include JsonQuery field + buggyJsonBytes, err := json.Marshal(buggyQuery) + assert.NoError(err) + + t.Logf("Buggy JSON size: %d bytes", len(buggyJsonBytes)) + t.Logf("Buggy JSON content: %s", string(buggyJsonBytes)) + + // This would create a JSON with nested JSON, potentially causing parsing issues + var parsed map[string]interface{} + err = json.Unmarshal(buggyJsonBytes, &parsed) + assert.NoError(err) // JSON is still valid, but contains extra data + + // Check if this creates the size we're looking for + if len(buggyJsonBytes) >= 1090 && len(buggyJsonBytes) <= 1100 { + t.Logf("🎯 ROOT CAUSE FOUND: JsonQuery field inclusion creates %d bytes", len(buggyJsonBytes)) + } + }) + + t.Run("root_cause_2_buffer_reuse_contamination", func(t *testing.T) { + // Test if buffer reuse might cause contamination + client := CreateTestClient() + + // Simulate multiple requests that might contaminate buffers + for i := 0; i < 3; i++ { + variables := map[string]interface{}{ + "group_description": fmt.Sprintf("Request %d: This a group for serious buyers and sellers\nOf uber eats and Door dash account \nRippers are not allowed in this group😈", i), + "user_name": "2eba巴结", + "request_id": fmt.Sprintf("req_%d", i), + } + + query := "mutation storeMessage($group_description: String!, $user_name: String!, $request_id: String) { insert_message(group_description: $group_description, user_name: $user_name, request_id: $request_id) { id } }" + compiledQuery := client.compileQuery(query, variables) + + t.Logf("Request %d JSON size: %d bytes", i, len(compiledQuery.JsonQuery)) + + // Check for any unexpected content + assert.True(json.Valid(compiledQuery.JsonQuery)) + } + }) + + t.Run("root_cause_3_encoding_header_mismatch", func(t *testing.T) { + // Test if Content-Type charset mismatch causes issues + client := CreateTestClient() + + variables := map[string]interface{}{ + "group_description": "This a group for serious buyers and sellers\nOf uber eats and Door dash account \nRippers are not allowed in this group😈", + "user_name": "2eba巴结", + } + + query := "mutation storeMessage($group_description: String!, $user_name: String!) { insert_message(group_description: $group_description, user_name: $user_name) { id } }" + compiledQuery := client.compileQuery(query, variables) + + // Test different charset declarations + charsets := []string{ + "application/json", + "application/json; charset=utf-8", + "application/json; charset=UTF-8", + "application/json; charset=iso-8859-1", // Wrong charset + } + + for _, contentType := range charsets { + t.Logf("Testing Content-Type: %s", contentType) + + // The JSON should be the same regardless of Content-Type header + assert.True(json.Valid(compiledQuery.JsonQuery)) + assert.Equal(356, len(compiledQuery.JsonQuery)) // Should be consistent + } + }) + + t.Run("root_cause_4_unicode_character_validation", func(t *testing.T) { + // Test the exact Unicode characters from production logs + client := CreateTestClient() + + // Test various Unicode scenarios that might cause issues + unicodeScenarios := []struct { + name string + userName string + description string + expectValid bool + }{ + { + name: "production_case_exact", + userName: "2eba巴结", + description: "This a group for serious buyers and sellers\nOf uber eats and Door dash account \nRippers are not allowed in this group😈", + expectValid: true, + }, + { + name: "mixed_unicode_emojis", + userName: "用户🎯💻🔥", + description: "Test with multiple emojis: 🚀🎉💡🔧⚡️🌟", + expectValid: true, + }, + { + name: "chinese_japanese_korean", + userName: "测试用户한국어日本語", + description: "Mixed CJK characters: 中文 한국어 日本語", + expectValid: true, + }, + { + name: "special_unicode_chars", + userName: "user™®©", + description: "Special symbols: ™®©±×÷≠≤≥∞∑∏∆√∫", + expectValid: true, + }, + } + + for _, scenario := range unicodeScenarios { + t.Run(scenario.name, func(t *testing.T) { + variables := map[string]interface{}{ + "group_description": scenario.description, + "user_name": scenario.userName, + } + + query := "mutation storeMessage($group_description: String!, $user_name: String!) { insert_message(group_description: $group_description, user_name: $user_name) { id } }" + compiledQuery := client.compileQuery(query, variables) + + // Validate UTF-8 encoding + assert.True(utf8.Valid(compiledQuery.JsonQuery), "JSON should be valid UTF-8") + assert.True(json.Valid(compiledQuery.JsonQuery), "JSON should be valid") + + // Check for any jsonQuery field inclusion + assert.False(bytes.Contains(compiledQuery.JsonQuery, []byte(`"jsonQuery"`)), "Should not contain jsonQuery field") + + // Log the size for analysis + t.Logf("%s: JSON size = %d bytes", scenario.name, len(compiledQuery.JsonQuery)) + t.Logf("Content: %s", string(compiledQuery.JsonQuery)) + + // Test with actual server + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body := make([]byte, r.ContentLength) + n, _ := r.Body.Read(body) + actualBody := body[:n] + + // Validate the request body + assert.True(utf8.Valid(actualBody), "Request body should be valid UTF-8") + assert.True(json.Valid(actualBody), "Request body should be valid JSON") + assert.False(bytes.Contains(actualBody, []byte(`"jsonQuery"`)), "Request should not contain jsonQuery field") + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"data":{"result":"success"}}`)) + })) + defer server.Close() + + qe := &QueryExecutor{ + BaseClient: client, + Query: compiledQuery.JsonQuery, + Headers: map[string]interface{}{"Content-Type": "application/json; charset=utf-8"}, + CacheKey: "no-cache", + Retries: false, + } + qe.endpoint = server.URL + qe.client = server.Client() + qe.cache = client.cache + + result, err := qe.executeQuery() + if scenario.expectValid { + assert.NoError(err, "Unicode request should succeed") + assert.NotNil(result) + } + }) + } + }) +} + +// TestDiagnosticLogging tests the new diagnostic logging functionality +func TestDiagnosticLogging(t *testing.T) { + assert := assertions.New(t) + + t.Run("should_log_request_body_analysis", func(t *testing.T) { + // Create a server that captures and analyzes the request + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body := make([]byte, r.ContentLength) + n, _ := r.Body.Read(body) + actualBody := body[:n] + + // Verify Content-Type includes charset + contentType := r.Header.Get("Content-Type") + if !strings.Contains(contentType, "charset=utf-8") { + t.Errorf("Content-Type should include charset=utf-8, got: %s", contentType) + } + + // Verify body size matches Content-Length + if int64(len(actualBody)) != r.ContentLength { + t.Errorf("Body size mismatch: Content-Length=%d, actual=%d", r.ContentLength, len(actualBody)) + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"data":{"result":"success"}}`)) + })) + defer server.Close() + + client := CreateTestClient() + + // Test with the production case + variables := map[string]interface{}{ + "group_description": "This a group for serious buyers and sellers\nOf uber eats and Door dash account \nRippers are not allowed in this group😈", + "user_name": "2eba巴结", + } + + query := "mutation storeMessage($group_description: String!, $user_name: String!) { insert_message(group_description: $group_description, user_name: $user_name) { id } }" + compiledQuery := client.compileQuery(query, variables) + + qe := &QueryExecutor{ + BaseClient: client, + Query: compiledQuery.JsonQuery, + Headers: map[string]interface{}{"Content-Type": "application/json; charset=utf-8"}, + CacheKey: "no-cache", + Retries: false, + } + qe.endpoint = server.URL + qe.client = server.Client() + qe.cache = client.cache + + result, err := qe.executeQuery() + assert.NoError(err) + assert.NotNil(result) + + // The diagnostic logging should have been triggered + // We can't easily capture the logs in this test, but we can verify the request succeeded + t.Logf("Request completed successfully with diagnostic logging") + t.Logf("Request body size: %d bytes", len(compiledQuery.JsonQuery)) + }) + + t.Run("should_detect_jsonQuery_field_inclusion", func(t *testing.T) { + // Test the validation function directly + client := CreateTestClient() + + // Create a problematic JSON with jsonQuery field + problematicJSON := `{ + "query": "mutation test { result }", + "variables": {"test": "value"}, + "jsonQuery": "eyJxdWVyeSI6Im11dGF0aW9uIHRlc3QgeyByZXN1bHQgfSIsInZhcmlhYmxlcyI6eyJ0ZXN0IjoidmFsdWUifX0=" + }` + + // This should be detected by our validation + err := validateRequestBody([]byte(problematicJSON), client.Logger) + assert.Error(err, "Should detect jsonQuery field inclusion") + assert.Contains(err.Error(), "jsonQuery field found") + }) + + t.Run("should_validate_utf8_encoding", func(t *testing.T) { + client := CreateTestClient() + + // Test valid UTF-8 + validJSON := `{"query": "mutation test { result }", "variables": {"user": "2eba巴结"}}` + err := validateRequestBody([]byte(validJSON), client.Logger) + assert.NoError(err, "Valid UTF-8 should pass validation") + + // Test invalid UTF-8 (manually crafted invalid sequence) + invalidUTF8 := []byte(`{"query": "mutation test { result }", "variables": {"user": "test`) + invalidUTF8 = append(invalidUTF8, 0xFF, 0xFE) // Invalid UTF-8 sequence + invalidUTF8 = append(invalidUTF8, []byte(`"}}`)...) + + err = validateRequestBody(invalidUTF8, client.Logger) + assert.Error(err, "Invalid UTF-8 should fail validation") + assert.Contains(err.Error(), "invalid UTF-8") + }) + + t.Run("should_detect_null_bytes", func(t *testing.T) { + client := CreateTestClient() + + // Test JSON with null bytes + jsonWithNulls := []byte(`{"query": "mutation test { result }", "variables": {"user": "test`) + jsonWithNulls = append(jsonWithNulls, 0x00) // Null byte + jsonWithNulls = append(jsonWithNulls, []byte(`"}}`)...) + + err := validateRequestBody(jsonWithNulls, client.Logger) + assert.Error(err, "JSON with null bytes should fail validation") + assert.Contains(err.Error(), "null bytes") + }) + + t.Run("should_test_exact_production_size_scenario", func(t *testing.T) { + client := CreateTestClient() + + // Test the exact scenario that produces ~1093 bytes + variables := map[string]interface{}{ + "group_description": "This a group for serious buyers and sellers\nOf uber eats and Door dash account \nRippers are not allowed in this group😈", + "user_name": "2eba巴结", + } + + // Test with a longer query that might reach 1093 bytes + longQuery := `mutation storeMessage( + $group_description: String!, + $user_name: String!, + $additional_metadata: String, + $client_info: String, + $request_timestamp: String, + $platform_data: String + ) { + insert_message( + group_description: $group_description, + user_name: $user_name, + additional_metadata: $additional_metadata, + client_info: $client_info, + request_timestamp: $request_timestamp, + platform_data: $platform_data + ) { + id + created_at + updated_at + status + metadata { + source + platform + version + client_info + request_id + processing_time + } + user_info { + name + id + permissions + } + } + }` + + // Add additional variables to increase size + extendedVariables := make(map[string]interface{}) + for k, v := range variables { + extendedVariables[k] = v + } + extendedVariables["additional_metadata"] = `{"client": "web", "version": "1.0.0", "features": ["unicode", "emoji"]}` + extendedVariables["client_info"] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" + extendedVariables["request_timestamp"] = "2023-05-23T18:01:37.000Z" + extendedVariables["platform_data"] = `{"os": "windows", "browser": "chrome", "screen": "1920x1080"}` + + compiledQuery := client.compileQuery(longQuery, extendedVariables) + size := len(compiledQuery.JsonQuery) + + t.Logf("Extended query size: %d bytes", size) + + if size >= 1090 && size <= 1100 { + t.Logf("🎯 FOUND SIZE MATCH: %d bytes (target: 1093)", size) + + // Validate this doesn't contain jsonQuery field + assert.False(bytes.Contains(compiledQuery.JsonQuery, []byte(`"jsonQuery"`)), "Should not contain jsonQuery field") + assert.True(json.Valid(compiledQuery.JsonQuery), "Should be valid JSON") + assert.True(utf8.Valid(compiledQuery.JsonQuery), "Should be valid UTF-8") + } + + // Test with server to ensure no trailing garbage + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body := make([]byte, r.ContentLength) + n, _ := r.Body.Read(body) + actualBody := body[:n] + + // Strict validation + decoder := json.NewDecoder(bytes.NewReader(actualBody)) + var request map[string]interface{} + err := decoder.Decode(&request) + if err != nil { + t.Errorf("JSON decode error: %s", err.Error()) + w.WriteHeader(http.StatusBadRequest) + return + } + + // Check for trailing data + remaining := decoder.Buffered() + if remaining != nil { + buf := make([]byte, 10) + n, _ := remaining.Read(buf) + if n > 0 { + t.Errorf("Found trailing data: %s", string(buf[:n])) + w.WriteHeader(http.StatusBadRequest) + return + } + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"data":{"result":"success"}}`)) + })) + defer server.Close() + + qe := &QueryExecutor{ + BaseClient: client, + Query: compiledQuery.JsonQuery, + Headers: map[string]interface{}{"Content-Type": "application/json; charset=utf-8"}, + CacheKey: "no-cache", + Retries: false, + } + qe.endpoint = server.URL + qe.client = server.Client() + qe.cache = client.cache + + result, err := qe.executeQuery() + assert.NoError(err, "Extended query should succeed without trailing garbage") + assert.NotNil(result) + }) + + t.Run("should_demonstrate_diagnostic_logging_output", func(t *testing.T) { + // This test specifically demonstrates the diagnostic logging in action + client := CreateTestClient() + + // Use the exact production case + variables := map[string]interface{}{ + "group_description": "This a group for serious buyers and sellers\nOf uber eats and Door dash account \nRippers are not allowed in this group😈", + "user_name": "2eba巴结", + } + + query := "mutation storeMessage($group_description: String!, $user_name: String!) { insert_message(group_description: $group_description, user_name: $user_name) { id } }" + compiledQuery := client.compileQuery(query, variables) + + // Create a server that will receive the request with diagnostic logging + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Logf("Server received request with Content-Type: %s", r.Header.Get("Content-Type")) + t.Logf("Server received Content-Length: %d", r.ContentLength) + + body := make([]byte, r.ContentLength) + n, _ := r.Body.Read(body) + actualBody := body[:n] + + t.Logf("Server read %d bytes", len(actualBody)) + t.Logf("Request body first 100 chars: %s", string(actualBody[:min(100, len(actualBody))])) + t.Logf("Request body last 50 chars: %s", string(actualBody[max(0, len(actualBody)-50):])) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"data":{"result":"success"}}`)) + })) + defer server.Close() + + qe := &QueryExecutor{ + BaseClient: client, + Query: compiledQuery.JsonQuery, + Headers: map[string]interface{}{"Content-Type": "application/json; charset=utf-8"}, + CacheKey: "no-cache", + Retries: false, + } + qe.endpoint = server.URL + qe.client = server.Client() + qe.cache = client.cache + + t.Logf("About to execute query with diagnostic logging enabled...") + result, err := qe.executeQuery() + assert.NoError(err) + assert.NotNil(result) + t.Logf("Query executed successfully with full diagnostic logging") + }) +} diff --git a/unicode_debug_test.go b/unicode_debug_test.go new file mode 100644 index 0000000..cf42e09 --- /dev/null +++ b/unicode_debug_test.go @@ -0,0 +1,316 @@ +package gql + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "testing" + "unicode/utf8" + + assertions "github.com/stretchr/testify/assert" +) + +// TestUnicodeDebugging creates specific scenarios that might cause "trailing garbage" errors +func TestUnicodeDebugging(t *testing.T) { + assert := assertions.New(t) + + t.Run("should debug exact scenario from user report", func(t *testing.T) { + // Recreate the exact scenario from the user's log + client := CreateTestClient() + + // Exact data from the user's report + variables := map[string]interface{}{ + "group_description": "This a group for serious buyers and sellers\nOf uber eats and Door dash account \nRippers are not allowed in this group😈", + "user_name": "2eba巴结", + } + + query := "mutation storeMessage($group_description: String!, $user_name: String!) { insert_message(group_description: $group_description, user_name: $user_name) { id } }" + + // Compile the query + compiledQuery := client.compileQuery(query, variables) + assert.NotNil(compiledQuery) + + jsonBytes := compiledQuery.JsonQuery + t.Logf("JSON size: %d bytes", len(jsonBytes)) + t.Logf("JSON content: %s", string(jsonBytes)) + + // Check for potential issues + t.Logf("Is valid UTF-8: %v", utf8.Valid(jsonBytes)) + t.Logf("Is valid JSON: %v", json.Valid(jsonBytes)) + + // Check for any trailing bytes or null terminators + if len(jsonBytes) > 0 { + t.Logf("Last byte: 0x%02x (%c)", jsonBytes[len(jsonBytes)-1], jsonBytes[len(jsonBytes)-1]) + if len(jsonBytes) > 1 { + t.Logf("Second to last byte: 0x%02x (%c)", jsonBytes[len(jsonBytes)-2], jsonBytes[len(jsonBytes)-2]) + } + } + + // Check for any BOM (Byte Order Mark) + if len(jsonBytes) >= 3 && jsonBytes[0] == 0xEF && jsonBytes[1] == 0xBB && jsonBytes[2] == 0xBF { + t.Logf("WARNING: UTF-8 BOM detected at start of JSON") + } + + // Try to parse with different JSON parsers to see if there are differences + var parsed1 map[string]interface{} + err1 := json.Unmarshal(jsonBytes, &parsed1) + t.Logf("Standard json.Unmarshal error: %v", err1) + + // Check if there are any non-printable characters at the end + for i := len(jsonBytes) - 1; i >= 0 && i >= len(jsonBytes)-10; i-- { + b := jsonBytes[i] + if b < 32 && b != '\n' && b != '\r' && b != '\t' { + t.Logf("Non-printable character at position %d: 0x%02x", i, b) + } + } + + // Simulate the exact request size mentioned in the user's log (1093 bytes) + t.Logf("Expected size from user log: 1093 bytes, actual size: %d bytes", len(jsonBytes)) + if len(jsonBytes) != 1093 { + t.Logf("Size mismatch - this might indicate a different encoding or additional data") + } + }) + + t.Run("should test potential causes of trailing garbage", func(t *testing.T) { + client := CreateTestClient() + + // Test scenarios that might cause trailing garbage + testCases := []struct { + name string + variables map[string]interface{} + expectIssue bool + }{ + { + name: "double_encoding_scenario", + variables: map[string]interface{}{ + "json_string": `{"nested": "json with emoji 😈"}`, + }, + expectIssue: false, + }, + { + name: "escaped_unicode_scenario", + variables: map[string]interface{}{ + "escaped": "\\u1F608", // Escaped Unicode + }, + expectIssue: false, + }, + { + name: "mixed_encoding_scenario", + variables: map[string]interface{}{ + "mixed": "ASCII text with 😈 emoji and 巴结 Chinese", + }, + expectIssue: false, + }, + { + name: "large_unicode_scenario", + variables: map[string]interface{}{ + "large": "This a group for serious buyers and sellers\nOf uber eats and Door dash account \nRippers are not allowed in this group😈", + "user": "2eba巴结", + "extra": "Additional data to increase size", + }, + expectIssue: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + query := "mutation test($json_string: String, $escaped: String, $mixed: String, $large: String, $user: String, $extra: String) { test }" + compiledQuery := client.compileQuery(query, tc.variables) + + assert.NotNil(compiledQuery) + jsonBytes := compiledQuery.JsonQuery + + t.Logf("%s - JSON size: %d bytes", tc.name, len(jsonBytes)) + t.Logf("%s - Is valid UTF-8: %v", tc.name, utf8.Valid(jsonBytes)) + t.Logf("%s - Is valid JSON: %v", tc.name, json.Valid(jsonBytes)) + + // Try to unmarshal + var parsed map[string]interface{} + err := json.Unmarshal(jsonBytes, &parsed) + if tc.expectIssue { + assert.Error(err, "Expected error for %s", tc.name) + } else { + assert.NoError(err, "Should not error for %s", tc.name) + } + }) + } + }) + + t.Run("should test server response to problematic requests", func(t *testing.T) { + // Create a server that's very strict about JSON parsing + strictServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body := make([]byte, r.ContentLength) + n, _ := r.Body.Read(body) + actualBody := body[:n] + + t.Logf("Server received %d bytes", len(actualBody)) + t.Logf("Server received content: %s", string(actualBody)) + + // Try to parse with strict JSON decoder + decoder := json.NewDecoder(bytes.NewReader(actualBody)) + decoder.DisallowUnknownFields() + + var request map[string]interface{} + err := decoder.Decode(&request) + if err != nil { + t.Logf("Server JSON decode error: %s", err.Error()) + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(fmt.Sprintf(`{"error":"trailing garbage after JSON","code":"invalid-json","details":"%s"}`, err.Error()))) + return + } + + // Check if there's more data after the JSON + var extra interface{} + err = decoder.Decode(&extra) + if err == nil { + t.Logf("Server found trailing data after JSON") + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(`{"error":"trailing garbage after JSON","code":"invalid-json"}`)) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"data":{"result":"success"}}`)) + })) + defer strictServer.Close() + + client := CreateTestClient() + + variables := map[string]interface{}{ + "group_description": "This a group for serious buyers and sellers\nOf uber eats and Door dash account \nRippers are not allowed in this group😈", + "user_name": "2eba巴结", + } + + query := "mutation storeMessage($group_description: String!, $user_name: String!) { insert_message(group_description: $group_description, user_name: $user_name) { id } }" + compiledQuery := client.compileQuery(query, variables) + + qe := &QueryExecutor{ + BaseClient: client, + Query: compiledQuery.JsonQuery, + Headers: map[string]interface{}{"Content-Type": "application/json; charset=utf-8"}, + CacheKey: "no-cache", + Retries: false, + } + qe.endpoint = strictServer.URL + qe.client = strictServer.Client() + qe.cache = client.cache + + result, err := qe.executeQuery() + if err != nil { + t.Logf("Error from strict server: %s", err.Error()) + } else { + t.Logf("Strict server accepted the request successfully") + } + + assert.NoError(err, "Strict server should accept properly encoded JSON") + assert.NotNil(result) + }) + + t.Run("should analyze JSON encoder behavior", func(t *testing.T) { + client := CreateTestClient() + + // Test the exact data structure + query := &Query{ + Query: "mutation storeMessage($group_description: String!, $user_name: String!) { insert_message(group_description: $group_description, user_name: $user_name) { id } }", + Variables: map[string]interface{}{ + "group_description": "This a group for serious buyers and sellers\nOf uber eats and Door dash account \nRippers are not allowed in this group😈", + "user_name": "2eba巴结", + }, + } + + // Test different encoding methods + t.Run("using_json_Marshal", func(t *testing.T) { + jsonBytes, err := json.Marshal(query) + assert.NoError(err) + t.Logf("json.Marshal size: %d bytes", len(jsonBytes)) + t.Logf("json.Marshal valid: %v", json.Valid(jsonBytes)) + }) + + t.Run("using_convertToJSON", func(t *testing.T) { + jsonBytes := client.convertToJSON(query) + assert.NotNil(jsonBytes) + t.Logf("convertToJSON size: %d bytes", len(jsonBytes)) + t.Logf("convertToJSON valid: %v", json.Valid(jsonBytes)) + }) + + t.Run("using_json_Encoder", func(t *testing.T) { + var buf bytes.Buffer + encoder := json.NewEncoder(&buf) + encoder.SetEscapeHTML(false) + err := encoder.Encode(query) + assert.NoError(err) + + jsonBytes := buf.Bytes() + // Remove trailing newline that Encoder adds + if len(jsonBytes) > 0 && jsonBytes[len(jsonBytes)-1] == '\n' { + jsonBytes = jsonBytes[:len(jsonBytes)-1] + } + + t.Logf("json.Encoder size: %d bytes", len(jsonBytes)) + t.Logf("json.Encoder valid: %v", json.Valid(jsonBytes)) + }) + }) +} + +// TestUnicodeEncodingComparison compares different encoding approaches +func TestUnicodeEncodingComparison(t *testing.T) { + assert := assertions.New(t) + + // Test data that caused the original issue + testData := map[string]interface{}{ + "group_description": "This a group for serious buyers and sellers\nOf uber eats and Door dash account \nRippers are not allowed in this group😈", + "user_name": "2eba巴结", + } + + t.Run("compare_encoding_methods", func(t *testing.T) { + // Method 1: Standard json.Marshal + bytes1, err1 := json.Marshal(testData) + assert.NoError(err1) + + // Method 2: json.Encoder with SetEscapeHTML(false) + var buf2 bytes.Buffer + enc2 := json.NewEncoder(&buf2) + enc2.SetEscapeHTML(false) + err2 := enc2.Encode(testData) + assert.NoError(err2) + bytes2 := buf2.Bytes() + if len(bytes2) > 0 && bytes2[len(bytes2)-1] == '\n' { + bytes2 = bytes2[:len(bytes2)-1] + } + + // Method 3: json.Encoder with SetEscapeHTML(true) - default + var buf3 bytes.Buffer + enc3 := json.NewEncoder(&buf3) + enc3.SetEscapeHTML(true) + err3 := enc3.Encode(testData) + assert.NoError(err3) + bytes3 := buf3.Bytes() + if len(bytes3) > 0 && bytes3[len(bytes3)-1] == '\n' { + bytes3 = bytes3[:len(bytes3)-1] + } + + t.Logf("Method 1 (json.Marshal) size: %d", len(bytes1)) + t.Logf("Method 2 (Encoder, EscapeHTML=false) size: %d", len(bytes2)) + t.Logf("Method 3 (Encoder, EscapeHTML=true) size: %d", len(bytes3)) + + t.Logf("Method 1 content: %s", string(bytes1)) + t.Logf("Method 2 content: %s", string(bytes2)) + t.Logf("Method 3 content: %s", string(bytes3)) + + // All should be valid + assert.True(json.Valid(bytes1)) + assert.True(json.Valid(bytes2)) + assert.True(json.Valid(bytes3)) + + // Check for differences + if !bytes.Equal(bytes1, bytes2) { + t.Logf("Difference between Marshal and Encoder(EscapeHTML=false)") + } + if !bytes.Equal(bytes2, bytes3) { + t.Logf("Difference between EscapeHTML=false and EscapeHTML=true") + } + }) +} diff --git a/unicode_encoding_test.go b/unicode_encoding_test.go new file mode 100644 index 0000000..26edceb --- /dev/null +++ b/unicode_encoding_test.go @@ -0,0 +1,367 @@ +package gql + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" + + assertions "github.com/stretchr/testify/assert" +) + +// TestUnicodeEncodingIssue reproduces the "trailing garbage" error with emojis and special characters +func TestUnicodeEncodingIssue(t *testing.T) { + assert := assertions.New(t) + + t.Run("should handle emoji and Chinese characters in GraphQL variables", func(t *testing.T) { + // Create a test server that simulates the problematic scenario + var receivedBody []byte + var receivedContentType string + var receivedContentLength int64 + + testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Capture the request details for analysis + receivedContentType = r.Header.Get("Content-Type") + receivedContentLength = r.ContentLength + + // Read the request body to analyze the encoding + body := make([]byte, r.ContentLength) + r.Body.Read(body) + receivedBody = body + + // Log the received data for debugging + t.Logf("Received Content-Type: %s", receivedContentType) + t.Logf("Received Content-Length: %d", receivedContentLength) + t.Logf("Received Body (first 200 chars): %s", string(body[:min(200, len(body))])) + t.Logf("Received Body (hex): %x", body[:min(50, len(body))]) + + // Check if the JSON is valid + var parsedRequest map[string]interface{} + err := json.Unmarshal(body, &parsedRequest) + if err != nil { + t.Logf("JSON parsing error: %s", err.Error()) + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(`{"error":"trailing garbage after JSON","code":"invalid-json"}`)) + return + } + + // Successful response + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"data":{"result":"success"}}`)) + })) + defer testServer.Close() + + client := CreateTestClient() + + // Test data that reproduces the issue + variables := map[string]interface{}{ + "group_description": "This a group for serious buyers and sellers\nOf uber eats and Door dash account \nRippers are not allowed in this group😈", + "user_name": "2eba巴结", + } + + query := "mutation storeMessage($group_description: String!, $user_name: String!) { insert_message(group_description: $group_description, user_name: $user_name) { id } }" + + // Compile the query to see how it's encoded + compiledQuery := client.compileQuery(query, variables) + assert.NotNil(compiledQuery) + assert.NotNil(compiledQuery.JsonQuery) + + t.Logf("Compiled Query JSON: %s", string(compiledQuery.JsonQuery)) + t.Logf("Compiled Query JSON (hex): %x", compiledQuery.JsonQuery) + + // Check if the JSON is valid UTF-8 + assert.True(json.Valid(compiledQuery.JsonQuery), "Compiled JSON should be valid") + + // Verify the JSON contains the Unicode characters correctly + var parsedQuery map[string]interface{} + err := json.Unmarshal(compiledQuery.JsonQuery, &parsedQuery) + assert.NoError(err) + + variables_parsed := parsedQuery["variables"].(map[string]interface{}) + assert.Equal("This a group for serious buyers and sellers\nOf uber eats and Door dash account \nRippers are not allowed in this group😈", variables_parsed["group_description"]) + assert.Equal("2eba巴结", variables_parsed["user_name"]) + + // Execute the query + qe := &QueryExecutor{ + BaseClient: client, + Query: compiledQuery.JsonQuery, + Headers: map[string]interface{}{"Content-Type": "application/json; charset=utf-8"}, + CacheKey: "no-cache", + Retries: false, + } + qe.endpoint = testServer.URL + qe.client = testServer.Client() + qe.cache = client.cache + + result, err := qe.executeQuery() + + // Analyze the request that was sent + t.Logf("Request body size: %d bytes", len(receivedBody)) + t.Logf("Content-Type sent: %s", receivedContentType) + + // Check if the request was properly encoded + if len(receivedBody) > 0 { + var receivedRequest map[string]interface{} + parseErr := json.Unmarshal(receivedBody, &receivedRequest) + if parseErr != nil { + t.Logf("Server couldn't parse JSON: %s", parseErr.Error()) + t.Logf("Raw body: %s", string(receivedBody)) + } else { + t.Logf("Server successfully parsed JSON") + if vars, ok := receivedRequest["variables"].(map[string]interface{}); ok { + t.Logf("Received group_description: %s", vars["group_description"]) + t.Logf("Received user_name: %s", vars["user_name"]) + } + } + } + + // The test should pass - no trailing garbage errors + assert.NoError(err, "Should not get trailing garbage errors with Unicode characters") + assert.NotNil(result) + }) + + t.Run("should properly encode various Unicode characters", func(t *testing.T) { + client := CreateTestClient() + + testCases := []struct { + name string + description string + variables map[string]interface{} + }{ + { + name: "emoji_only", + description: "Test with various emojis", + variables: map[string]interface{}{ + "message": "Hello 😀😃😄😁😆😅😂🤣😊😇🙂🙃😉😌😍🥰😘😗😙😚😋😛😝😜🤪🤨🧐🤓😎🤩🥳😏😒😞😔😟😕🙁☹️😣😖😫😩🥺😢😭😤😠😡🤬🤯😳🥵🥶😱😨😰😥😓🤗🤔🤭🤫🤥😶😐😑😬🙄😯😦😧😮😲🥱😴🤤😪😵🤐🥴🤢🤮🤧😷🤒🤕🤑🤠😈👿👹👺🤡💩👻💀☠️👽👾🤖🎃😺😸😹😻😼😽🙀😿😾", + }, + }, + { + name: "chinese_characters", + description: "Test with Chinese characters", + variables: map[string]interface{}{ + "name": "张三李四王五赵六", + "address": "北京市朝阳区建国门外大街1号", + "company": "中国国际贸易中心有限公司", + }, + }, + { + name: "japanese_characters", + description: "Test with Japanese characters", + variables: map[string]interface{}{ + "name": "田中太郎", + "address": "東京都渋谷区渋谷1-1-1", + "message": "こんにちは、世界!", + }, + }, + { + name: "korean_characters", + description: "Test with Korean characters", + variables: map[string]interface{}{ + "name": "김철수", + "address": "서울특별시 강남구 테헤란로 123", + "message": "안녕하세요, 세계!", + }, + }, + { + name: "arabic_characters", + description: "Test with Arabic characters", + variables: map[string]interface{}{ + "name": "محمد أحمد", + "address": "الرياض، المملكة العربية السعودية", + "message": "مرحبا بالعالم!", + }, + }, + { + name: "mixed_unicode", + description: "Test with mixed Unicode characters", + variables: map[string]interface{}{ + "message": "Hello 世界 🌍 مرحبا こんにちは 안녕하세요 Здравствуй Bonjour ¡Hola! Γεια σας", + "symbols": "©®™€£¥₹₽¢₩₪₫₱₡₦₨₹₽₴₸₼₾₿", + "math": "∑∏∫∂∇∆∞≠≤≥±×÷√∛∜∝∞", + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + query := "mutation testUnicode($message: String, $name: String, $address: String, $company: String, $symbols: String, $math: String) { test }" + + // Compile the query + compiledQuery := client.compileQuery(query, tc.variables) + assert.NotNil(compiledQuery, "Query should compile successfully for %s", tc.description) + assert.NotNil(compiledQuery.JsonQuery, "JSON query should be generated for %s", tc.description) + + // Verify the JSON is valid + assert.True(json.Valid(compiledQuery.JsonQuery), "JSON should be valid for %s", tc.description) + + // Verify we can parse it back + var parsedQuery map[string]interface{} + err := json.Unmarshal(compiledQuery.JsonQuery, &parsedQuery) + assert.NoError(err, "Should be able to parse JSON for %s", tc.description) + + // Verify the variables are preserved correctly + if parsedVars, ok := parsedQuery["variables"].(map[string]interface{}); ok { + for key, expectedValue := range tc.variables { + actualValue := parsedVars[key] + assert.Equal(expectedValue, actualValue, "Variable %s should be preserved correctly for %s", key, tc.description) + } + } + + t.Logf("✓ %s: JSON size: %d bytes", tc.description, len(compiledQuery.JsonQuery)) + }) + } + }) + + t.Run("should handle Content-Type with charset specification", func(t *testing.T) { + var receivedContentType string + var receivedBody []byte + + testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + receivedContentType = r.Header.Get("Content-Type") + body := make([]byte, r.ContentLength) + r.Body.Read(body) + receivedBody = body + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"data":{"result":"success"}}`)) + })) + defer testServer.Close() + + client := CreateTestClient() + + variables := map[string]interface{}{ + "emoji_text": "Test with emoji 😈 and Chinese 巴结", + } + + query := "mutation test($emoji_text: String!) { test(text: $emoji_text) }" + compiledQuery := client.compileQuery(query, variables) + + // Test different Content-Type headers + contentTypes := []string{ + "application/json", + "application/json; charset=utf-8", + "application/json; charset=UTF-8", + } + + for _, contentType := range contentTypes { + t.Run("content_type_"+strings.ReplaceAll(contentType, " ", "_"), func(t *testing.T) { + qe := &QueryExecutor{ + BaseClient: client, + Query: compiledQuery.JsonQuery, + Headers: map[string]interface{}{"Content-Type": contentType}, + CacheKey: "no-cache", + Retries: false, + } + qe.endpoint = testServer.URL + qe.client = testServer.Client() + qe.cache = client.cache + + result, err := qe.executeQuery() + + assert.NoError(err, "Should handle Content-Type: %s", contentType) + assert.NotNil(result) + assert.Equal(contentType, receivedContentType, "Content-Type should be preserved") + + // Verify the body is valid JSON with Unicode characters + var parsedBody map[string]interface{} + parseErr := json.Unmarshal(receivedBody, &parsedBody) + assert.NoError(parseErr, "Request body should be valid JSON") + + if vars, ok := parsedBody["variables"].(map[string]interface{}); ok { + assert.Equal("Test with emoji 😈 and Chinese 巴结", vars["emoji_text"]) + } + }) + } + }) +} + +// TestUnicodeEncodingEdgeCases tests edge cases that might cause encoding issues +func TestUnicodeEncodingEdgeCases(t *testing.T) { + assert := assertions.New(t) + client := CreateTestClient() + + t.Run("should handle null bytes and control characters", func(t *testing.T) { + // Test with potentially problematic characters + variables := map[string]interface{}{ + "text_with_nulls": "Text\x00with\x00null\x00bytes", + "text_with_controls": "Text\x01\x02\x03\x04\x05with\x06\x07\x08controls", + "text_with_tabs": "Text\twith\ttabs\tand\nnewlines\r\n", + } + + query := "mutation test($text_with_nulls: String, $text_with_controls: String, $text_with_tabs: String) { test }" + compiledQuery := client.compileQuery(query, variables) + + assert.NotNil(compiledQuery) + assert.NotNil(compiledQuery.JsonQuery) + + // The JSON should be valid (Go's json package handles escaping) + assert.True(json.Valid(compiledQuery.JsonQuery)) + + // Verify we can parse it back + var parsedQuery map[string]interface{} + err := json.Unmarshal(compiledQuery.JsonQuery, &parsedQuery) + assert.NoError(err) + + t.Logf("JSON with control characters: %s", string(compiledQuery.JsonQuery)) + }) + + t.Run("should handle very long Unicode strings", func(t *testing.T) { + // Create a very long string with Unicode characters + longUnicodeString := strings.Repeat("Hello 世界 😀 مرحبا ", 1000) + + variables := map[string]interface{}{ + "long_unicode": longUnicodeString, + } + + query := "mutation test($long_unicode: String!) { test }" + compiledQuery := client.compileQuery(query, variables) + + assert.NotNil(compiledQuery) + assert.NotNil(compiledQuery.JsonQuery) + assert.True(json.Valid(compiledQuery.JsonQuery)) + + t.Logf("Long Unicode JSON size: %d bytes", len(compiledQuery.JsonQuery)) + + // Verify the content is preserved + var parsedQuery map[string]interface{} + err := json.Unmarshal(compiledQuery.JsonQuery, &parsedQuery) + assert.NoError(err) + + if vars, ok := parsedQuery["variables"].(map[string]interface{}); ok { + assert.Equal(longUnicodeString, vars["long_unicode"]) + } + }) + + t.Run("should handle Unicode normalization edge cases", func(t *testing.T) { + // Test different Unicode normalization forms + variables := map[string]interface{}{ + "composed": "é", // U+00E9 (composed) + "decomposed": "e\u0301", // U+0065 U+0301 (decomposed) + "ligature": "fi", // U+FB01 (ligature) + "surrogate": "𝕳𝖊𝖑𝖑𝖔", // Mathematical script letters (surrogate pairs) + } + + query := "mutation test($composed: String, $decomposed: String, $ligature: String, $surrogate: String) { test }" + compiledQuery := client.compileQuery(query, variables) + + assert.NotNil(compiledQuery) + assert.NotNil(compiledQuery.JsonQuery) + assert.True(json.Valid(compiledQuery.JsonQuery)) + + // Verify all forms are preserved + var parsedQuery map[string]interface{} + err := json.Unmarshal(compiledQuery.JsonQuery, &parsedQuery) + assert.NoError(err) + + if vars, ok := parsedQuery["variables"].(map[string]interface{}); ok { + for key, expectedValue := range variables { + assert.Equal(expectedValue, vars[key], "Unicode form should be preserved for %s", key) + } + } + + t.Logf("Unicode normalization test JSON: %s", string(compiledQuery.JsonQuery)) + }) +}