package utils import ( "encoding/json" "fmt" "testing" ) func TestParseSQL(t *testing.T) { tests := []struct { name string sql string wantIsSelect bool wantTables []string wantSelect []string wantWhere []string wantWhereText string }{ { name: "Simple SELECT", sql: "SELECT id, name, age FROM users WHERE age > 18", wantIsSelect: true, wantTables: []string{"users"}, wantSelect: []string{"id", "name", "age"}, wantWhere: []string{"age"}, wantWhereText: "age > 18", }, { name: "SELECT with multiple WHERE conditions", sql: "SELECT u.id, u.name FROM users u WHERE u.age > 18 AND u.status = 'active'", wantIsSelect: true, wantTables: []string{"users"}, wantSelect: []string{"id", "name"}, wantWhere: []string{"age", "status"}, wantWhereText: "u.age > 18 AND u.status = 'active'", }, { name: "SELECT with JOIN", sql: "SELECT u.name, o.total FROM users u JOIN orders o ON u.id = o.user_id WHERE o.total > 100", wantIsSelect: true, wantTables: []string{"users", "orders"}, wantSelect: []string{"name", "total"}, wantWhere: []string{"total"}, wantWhereText: "o.total > 100", }, { name: "SELECT with aggregate functions", sql: "SELECT COUNT(id), AVG(score) FROM students WHERE grade = 'A'", wantIsSelect: true, wantTables: []string{"students"}, wantSelect: []string{"id", "score"}, wantWhere: []string{"grade"}, wantWhereText: "grade = 'A'", }, { name: "SELECT with complex WHERE", sql: "SELECT * FROM products WHERE price BETWEEN 10 AND 100 AND category IN ('electronics', 'books')", wantIsSelect: true, wantTables: []string{"products"}, wantSelect: []string{}, wantWhere: []string{"price", "category"}, wantWhereText: "price BETWEEN 10 AND 100 AND category IN ('electronics', 'books')", }, { name: "INSERT statement", sql: "INSERT INTO users (name, age) VALUES ('John', 25)", wantIsSelect: false, }, { name: "UPDATE statement", sql: "UPDATE users SET age = 26 WHERE id = 1", wantIsSelect: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := ParseSQL(tt.sql) // Print result for debugging resultJSON, _ := json.MarshalIndent(result, "", " ") fmt.Printf("\nTest: %s\nResult:\n%s\n", tt.name, string(resultJSON)) if result.IsSelect != tt.wantIsSelect { t.Errorf("IsSelect = %v, want %v", result.IsSelect, tt.wantIsSelect) } if !tt.wantIsSelect { // For non-SELECT statements, just check IsSelect return } if result.ParseError != "" { t.Errorf("ParseError = %v, want empty", result.ParseError) } // Check tables if len(result.TableNames) != len(tt.wantTables) { t.Errorf("TableNames count = %d, want %d. Got: %v, Want: %v", len(result.TableNames), len(tt.wantTables), result.TableNames, tt.wantTables) } else { for i, table := range tt.wantTables { if i < len(result.TableNames) && result.TableNames[i] != table { t.Errorf("TableNames[%d] = %v, want %v", i, result.TableNames[i], table) } } } // Check SELECT fields if len(result.SelectFields) != len(tt.wantSelect) { t.Errorf("SelectFields count = %d, want %d. Got: %v, Want: %v", len(result.SelectFields), len(tt.wantSelect), result.SelectFields, tt.wantSelect) } // Check WHERE fields if len(result.WhereFields) != len(tt.wantWhere) { t.Errorf("WhereFields count = %d, want %d. Got: %v, Want: %v", len(result.WhereFields), len(tt.wantWhere), result.WhereFields, tt.wantWhere) } // Check WHERE clause text if result.WhereClause != tt.wantWhereText { t.Errorf("WhereClause = %q, want %q", result.WhereClause, tt.wantWhereText) } }) } } func ExampleParseSQL() { sql := "SELECT id, name, email FROM users WHERE age > 18 AND status = 'active'" result := ParseSQL(sql) fmt.Printf("Is SELECT: %v\n", result.IsSelect) fmt.Printf("Tables: %v\n", result.TableNames) fmt.Printf("SELECT fields: %v\n", result.SelectFields) fmt.Printf("WHERE fields: %v\n", result.WhereFields) fmt.Printf("WHERE clause: %s\n", result.WhereClause) // Output: // Is SELECT: true // Tables: [users] // SELECT fields: [id name email] // WHERE fields: [age status] // WHERE clause: age > 18 AND status = 'active' } func TestValidateSQL_TableNames(t *testing.T) { tests := []struct { name string sql string allowedTables []string wantValid bool wantErrorType string }{ { name: "Valid table name", sql: "SELECT * FROM users WHERE id = 1", allowedTables: []string{"users", "orders"}, wantValid: true, }, { name: "Invalid table name", sql: "SELECT * FROM products WHERE id = 1", allowedTables: []string{"users", "orders"}, wantValid: false, wantErrorType: "table_not_allowed", }, { name: "Multiple tables - all valid", sql: "SELECT * FROM users u JOIN orders o ON u.id = o.user_id", allowedTables: []string{"users", "orders"}, wantValid: true, }, { name: "Multiple tables - one invalid", sql: "SELECT * FROM users u JOIN products p ON u.id = p.user_id", allowedTables: []string{"users", "orders"}, wantValid: false, wantErrorType: "table_not_allowed", }, { name: "Case insensitive table names", sql: "SELECT * FROM USERS WHERE id = 1", allowedTables: []string{"users", "orders"}, wantValid: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { _, validation := ValidateSQL(tt.sql, WithAllowedTables(tt.allowedTables...)) if validation.Valid != tt.wantValid { t.Errorf("Valid = %v, want %v", validation.Valid, tt.wantValid) } if !tt.wantValid && len(validation.Errors) > 0 { if validation.Errors[0].Type != tt.wantErrorType { t.Errorf("Error type = %v, want %v", validation.Errors[0].Type, tt.wantErrorType) } } // Print validation result for debugging if !validation.Valid { validationJSON, _ := json.MarshalIndent(validation, "", " ") fmt.Printf("\nTest: %s\nValidation Result:\n%s\n", tt.name, string(validationJSON)) } }) } } func TestValidateSQL_InjectionRisk(t *testing.T) { tests := []struct { name string sql string wantValid bool wantErrorType string description string }{ { name: "Normal WHERE clause", sql: "SELECT * FROM users WHERE age > 18 AND status = 'active'", wantValid: true, description: "Should pass normal conditions", }, { name: "SQL injection with 1=1", sql: "SELECT * FROM users WHERE id = 1 OR 1=1", wantValid: false, wantErrorType: "sql_injection_risk", description: "Should detect 1=1 pattern", }, { name: "SQL injection with '1'='1'", sql: "SELECT * FROM users WHERE username = 'admin' OR '1'='1'", wantValid: false, wantErrorType: "sql_injection_risk", description: "Should detect '1'='1' pattern", }, { name: "SQL injection with 0=0", sql: "SELECT * FROM users WHERE 0=0", wantValid: false, wantErrorType: "sql_injection_risk", description: "Should detect 0=0 pattern", }, { name: "SQL injection with true", sql: "SELECT * FROM users WHERE true", wantValid: false, wantErrorType: "sql_injection_risk", description: "Should detect 'true' pattern", }, { name: "SQL injection with empty string comparison", sql: "SELECT * FROM users WHERE ''=''", wantValid: false, wantErrorType: "sql_injection_risk", description: "Should detect empty string comparison", }, { name: "SQL injection with 1=0", sql: "SELECT * FROM users WHERE 1=0", wantValid: false, wantErrorType: "sql_injection_risk", description: "Should detect 1=0 pattern", }, { name: "SQL injection with false", sql: "SELECT * FROM users WHERE false", wantValid: false, wantErrorType: "sql_injection_risk", description: "Should detect 'false' pattern", }, { name: "Complex injection with AND", sql: "SELECT * FROM users WHERE username = 'admin' AND 1=1", wantValid: false, wantErrorType: "sql_injection_risk", description: "Should detect 1=1 even with AND", }, { name: "Normal comparison with numbers", sql: "SELECT * FROM users WHERE status_code = 1", wantValid: true, description: "Should allow normal number comparisons", }, { name: "Normal string comparison", sql: "SELECT * FROM users WHERE name = 'John'", wantValid: true, description: "Should allow normal string comparisons", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { _, validation := ValidateSQL(tt.sql, WithInjectionRiskCheck()) if validation.Valid != tt.wantValid { t.Errorf("%s: Valid = %v, want %v", tt.description, validation.Valid, tt.wantValid) } if !tt.wantValid && len(validation.Errors) > 0 { found := false for _, err := range validation.Errors { if err.Type == tt.wantErrorType { found = true break } } if !found { t.Errorf("%s: Expected error type %v not found in errors", tt.description, tt.wantErrorType) } } // Print validation result for debugging if !validation.Valid { validationJSON, _ := json.MarshalIndent(validation, "", " ") fmt.Printf("\nTest: %s\nValidation Result:\n%s\n", tt.name, string(validationJSON)) } }) } } func TestValidateSQL_CombinedOptions(t *testing.T) { tests := []struct { name string sql string allowedTables []string wantValid bool wantErrorCnt int }{ { name: "Valid SQL with both checks", sql: "SELECT * FROM users WHERE age > 18", allowedTables: []string{"users", "orders"}, wantValid: true, wantErrorCnt: 0, }, { name: "Invalid table and injection risk", sql: "SELECT * FROM products WHERE 1=1", allowedTables: []string{"users", "orders"}, wantValid: false, wantErrorCnt: 2, // Both table and injection errors }, { name: "Valid table but injection risk", sql: "SELECT * FROM users WHERE id = 1 OR 1=1", allowedTables: []string{"users", "orders"}, wantValid: false, wantErrorCnt: 1, // Only injection error }, { name: "Invalid table but no injection", sql: "SELECT * FROM products WHERE age > 18", allowedTables: []string{"users", "orders"}, wantValid: false, wantErrorCnt: 1, // Only table error }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { _, validation := ValidateSQL(tt.sql, WithAllowedTables(tt.allowedTables...), WithInjectionRiskCheck(), ) if validation.Valid != tt.wantValid { t.Errorf("Valid = %v, want %v", validation.Valid, tt.wantValid) } if len(validation.Errors) != tt.wantErrorCnt { t.Errorf("Error count = %d, want %d", len(validation.Errors), tt.wantErrorCnt) } // Print validation result for debugging validationJSON, _ := json.MarshalIndent(validation, "", " ") fmt.Printf("\nTest: %s\nValidation Result:\n%s\n", tt.name, string(validationJSON)) }) } } func ExampleValidateSQL() { // Example 1: Validate table names sql1 := "SELECT * FROM users WHERE age > 18" _, validation1 := ValidateSQL(sql1, WithAllowedTables("users", "orders")) fmt.Printf("Example 1 - Valid: %v\n", validation1.Valid) // Example 2: Detect SQL injection sql2 := "SELECT * FROM users WHERE id = 1 OR 1=1" _, validation2 := ValidateSQL(sql2, WithInjectionRiskCheck()) fmt.Printf("Example 2 - Valid: %v\n", validation2.Valid) if !validation2.Valid { fmt.Printf("Error: %s\n", validation2.Errors[0].Message) } // Example 3: Combined validation sql3 := "SELECT * FROM products WHERE 1=1" _, validation3 := ValidateSQL(sql3, WithAllowedTables("users", "orders"), WithInjectionRiskCheck(), ) fmt.Printf("Example 3 - Valid: %v, Error count: %d\n", validation3.Valid, len(validation3.Errors)) // Output: // Example 1 - Valid: true // Example 2 - Valid: false // Error: High-risk SQL injection pattern detected // Example 3 - Valid: false, Error count: 2 } func TestInjectAndConditions(t *testing.T) { tests := []struct { name string sql string filter string want string }{ { name: "existing WHERE with ORDER BY", sql: "SELECT id, title FROM knowledges WHERE parse_status = 'completed' ORDER BY created_at DESC LIMIT 10", filter: "knowledges.tenant_id = 123", want: "SELECT id, title FROM knowledges WHERE knowledges.tenant_id = 123 AND (parse_status = 'completed') ORDER BY created_at DESC LIMIT 10", }, { name: "existing WHERE without tail clauses", sql: "SELECT id FROM knowledges WHERE enable_status = 'enabled'", filter: "knowledges.deleted_at IS NULL", want: "SELECT id FROM knowledges WHERE knowledges.deleted_at IS NULL AND (enable_status = 'enabled')", }, { name: "no WHERE with ORDER BY", sql: "SELECT id FROM knowledges ORDER BY created_at DESC", filter: "knowledges.tenant_id = 123", want: "SELECT id FROM knowledges WHERE knowledges.tenant_id = 123 ORDER BY created_at DESC", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := InjectAndConditions(tt.sql, tt.filter) if got != tt.want { t.Fatalf("InjectAndConditions() = %q, want %q", got, tt.want) } }) } }