mirror of
https://github.com/lukaszraczylo/go-simple-graphql.git
synced 2026-10-19 01:27:43 +00:00
fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! Library improvements.
This commit is contained in:
@@ -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")
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user