From 68127fe453c236c319c18904369894526a1ad831 Mon Sep 17 00:00:00 2001 From: PhatPhuckDave Date: Tue, 25 Mar 2025 19:20:42 +0100 Subject: [PATCH] Add more json tests To bring it in line with the xml ones --- processor/json_test.go | 673 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 673 insertions(+) diff --git a/processor/json_test.go b/processor/json_test.go index 33673d4..86e795a 100644 --- a/processor/json_test.go +++ b/processor/json_test.go @@ -1096,3 +1096,676 @@ func TestJSONProcessor_RootNodeModification(t *testing.T) { t.Errorf("Expected content to be:\n%s\n\nGot:\n%s", expected, result) } } + +// TestJSONProcessor_DateManipulation tests manipulating date strings in a JSON document +func TestJSONProcessor_DateManipulation(t *testing.T) { + content := `{ + "events": [ + { + "name": "Conference", + "date": "2023-06-15" + }, + { + "name": "Workshop", + "date": "2023-06-20" + } + ] + }` + + expected := `{ + "events": [ + { + "name": "Conference", + "date": "2023-07-15" + }, + { + "name": "Workshop", + "date": "2023-07-20" + } + ] + }` + + p := &JSONProcessor{} + result, modCount, matchCount, err := p.ProcessContent(content, "$.events[*].date", ` + local year, month, day = string.match(v, "(%d%d%d%d)-(%d%d)-(%d%d)") + -- Postpone events by 1 month + month = tonumber(month) + 1 + if month > 12 then + month = 1 + year = tonumber(year) + 1 + end + v = string.format("%04d-%02d-%s", tonumber(year), month, day) + `) + + if err != nil { + t.Fatalf("Error processing content: %v", err) + } + + if matchCount != 2 { + t.Errorf("Expected 2 matches, got %d", matchCount) + } + + if modCount != 2 { + t.Errorf("Expected 2 modifications, got %d", modCount) + } + + // Parse results as JSON objects for deep comparison rather than string comparison + var resultObj map[string]interface{} + var expectedObj map[string]interface{} + + if err := json.Unmarshal([]byte(result), &resultObj); err != nil { + t.Fatalf("Failed to parse result JSON: %v", err) + } + + if err := json.Unmarshal([]byte(expected), &expectedObj); err != nil { + t.Fatalf("Failed to parse expected JSON: %v", err) + } + + // Get the events arrays + resultEvents, ok := resultObj["events"].([]interface{}) + if !ok || len(resultEvents) != 2 { + t.Fatalf("Expected events array with 2 items in result") + } + + expectedEvents, ok := expectedObj["events"].([]interface{}) + if !ok || len(expectedEvents) != 2 { + t.Fatalf("Expected events array with 2 items in expected") + } + + // Check each event's date value + for i := 0; i < 2; i++ { + resultEvent, ok := resultEvents[i].(map[string]interface{}) + if !ok { + t.Fatalf("Expected event %d to be an object", i) + } + + expectedEvent, ok := expectedEvents[i].(map[string]interface{}) + if !ok { + t.Fatalf("Expected expected event %d to be an object", i) + } + + resultDate, ok := resultEvent["date"].(string) + if !ok { + t.Fatalf("Expected date in result event %d to be a string", i) + } + + expectedDate, ok := expectedEvent["date"].(string) + if !ok { + t.Fatalf("Expected date in expected event %d to be a string", i) + } + + if resultDate != expectedDate { + t.Errorf("Event %d: expected date %s, got %s", i, expectedDate, resultDate) + } + } +} + +// TestJSONProcessor_MathFunctions tests using math functions in JSON processing +func TestJSONProcessor_MathFunctions(t *testing.T) { + content := `{ + "measurements": [ + 3.14159, + 2.71828, + 1.41421 + ] + }` + + expected := `{ + "measurements": [ + 3, + 3, + 1 + ] + }` + + p := &JSONProcessor{} + result, modCount, matchCount, err := p.ProcessContent(content, "$.measurements[*]", "v = round(v)") + + if err != nil { + t.Fatalf("Error processing content: %v", err) + } + + if matchCount != 3 { + t.Errorf("Expected 3 matches, got %d", matchCount) + } + + if modCount != 3 { + t.Errorf("Expected 3 modifications, got %d", modCount) + } + + // Normalize whitespace for comparison + normalizedResult := normalizeWhitespace(result) + normalizedExpected := normalizeWhitespace(expected) + + if normalizedResult != normalizedExpected { + t.Errorf("Expected content to be:\n%s\n\nGot:\n%s", expected, result) + } +} + +// TestJSONProcessor_Error_InvalidJSON tests error handling for invalid JSON +func TestJSONProcessor_Error_InvalidJSON(t *testing.T) { + content := `{ + "unclosed": "value" + ` + + p := &JSONProcessor{} + _, _, _, err := p.ProcessContent(content, "$.unclosed", "v='modified'") + + if err == nil { + t.Errorf("Expected an error for invalid JSON, but got none") + } +} + +// TestJSONProcessor_Error_InvalidJSONPath tests error handling for invalid JSONPath +func TestJSONProcessor_Error_InvalidJSONPath(t *testing.T) { + content := `{ + "element": "value" + }` + + p := &JSONProcessor{} + _, _, _, err := p.ProcessContent(content, "[invalid path]", "v='modified'") + + if err == nil { + t.Errorf("Expected an error for invalid JSONPath, but got none") + } +} + +// TestJSONProcessor_Error_InvalidLua tests error handling for invalid Lua +func TestJSONProcessor_Error_InvalidLua(t *testing.T) { + content := `{ + "element": 123 + }` + + p := &JSONProcessor{} + _, _, _, err := p.ProcessContent(content, "$.element", "v = invalid_function()") + + if err == nil { + t.Errorf("Expected an error for invalid Lua, but got none") + } +} + +// TestJSONProcessor_Process_SpecialCharacters tests handling of special characters in JSON +func TestJSONProcessor_Process_SpecialCharacters(t *testing.T) { + content := `{ + "data": [ + "This & that", + "a < b", + "c > d", + "Quote: \"Hello\"" + ] + }` + + p := &JSONProcessor{} + result, modCount, matchCount, err := p.ProcessContent(content, "$.data[*]", "v = string.upper(v)") + + if err != nil { + t.Fatalf("Error processing content: %v", err) + } + + if matchCount != 4 { + t.Errorf("Expected 4 matches, got %d", matchCount) + } + + if modCount != 4 { + t.Errorf("Expected 4 modifications, got %d", modCount) + } + + // Parse the result to verify the content + var resultObj map[string]interface{} + if err := json.Unmarshal([]byte(result), &resultObj); err != nil { + t.Fatalf("Failed to parse result JSON: %v", err) + } + + data, ok := resultObj["data"].([]interface{}) + if !ok || len(data) != 4 { + t.Fatalf("Expected data array with 4 items") + } + + expectedValues := []string{ + "THIS & THAT", + "A < B", + "C > D", + "QUOTE: \"HELLO\"" + } + + for i, val := range data { + strVal, ok := val.(string) + if !ok { + t.Errorf("Expected item %d to be a string", i) + continue + } + + if strVal != expectedValues[i] { + t.Errorf("Item %d: expected %q, got %q", i, expectedValues[i], strVal) + } + } +} + +// TestJSONProcessor_AggregateCalculation tests calculating aggregated values from multiple fields +func TestJSONProcessor_AggregateCalculation(t *testing.T) { + content := `{ + "items": [ + { + "name": "Apple", + "price": 1.99, + "quantity": 10 + }, + { + "name": "Carrot", + "price": 0.99, + "quantity": 5 + } + ] + }` + + expected := `{ + "items": [ + { + "name": "Apple", + "price": 1.99, + "quantity": 10, + "total": 19.9 + }, + { + "name": "Carrot", + "price": 0.99, + "quantity": 5, + "total": 4.95 + } + ] + }` + + p := &JSONProcessor{} + result, modCount, matchCount, err := p.ProcessContent(content, "$.items[*]", ` + -- Calculate total from price and quantity + local price = v.price + local quantity = v.quantity + + -- Add new total field + v.total = price * quantity + `) + + if err != nil { + t.Fatalf("Error processing content: %v", err) + } + + if matchCount != 2 { + t.Errorf("Expected 2 matches, got %d", matchCount) + } + + if modCount != 2 { + t.Errorf("Expected 2 modifications, got %d", modCount) + } + + // Normalize whitespace for comparison + normalizedResult := normalizeWhitespace(result) + normalizedExpected := normalizeWhitespace(expected) + + if normalizedResult != normalizedExpected { + t.Errorf("Expected content to be:\n%s\n\nGot:\n%s", expected, result) + } +} + +// TestJSONProcessor_DataAnonymization tests anonymizing sensitive data +func TestJSONProcessor_DataAnonymization(t *testing.T) { + content := `{ + "contacts": [ + { + "name": "John Doe", + "email": "john.doe@example.com", + "phone": "123-456-7890" + }, + { + "name": "Jane Smith", + "email": "jane.smith@example.com", + "phone": "456-789-0123" + } + ] + }` + + p := &JSONProcessor{} + + // First pass: anonymize email addresses + result, modCount1, matchCount1, err := p.ProcessContent(content, "$.contacts[*].email", ` + -- Anonymize email + v = string.gsub(v, "@.+", "@anon.com") + local username = string.match(v, "(.+)@") + v = string.gsub(username, "%.", "") .. "@anon.com" + `) + + if err != nil { + t.Fatalf("Error processing email content: %v", err) + } + + // Second pass: anonymize phone numbers + result, modCount2, matchCount2, err := p.ProcessContent(result, "$.contacts[*].phone", ` + -- Mask phone numbers + v = string.gsub(v, "%d%d%d%-%d%d%d%-%d%d%d%d", function(match) + return string.sub(match, 1, 3) .. "-XXX-XXXX" + end) + `) + + if err != nil { + t.Fatalf("Error processing phone content: %v", err) + } + + // Total counts from both operations + matchCount := matchCount1 + matchCount2 + modCount := modCount1 + modCount2 + + if matchCount != 4 { + t.Errorf("Expected 4 total matches, got %d", matchCount) + } + + if modCount != 4 { + t.Errorf("Expected 4 total modifications, got %d", modCount) + } + + // Parse the resulting JSON for validating content + var resultObj map[string]interface{} + if err := json.Unmarshal([]byte(result), &resultObj); err != nil { + t.Fatalf("Failed to parse result JSON: %v", err) + } + + contacts, ok := resultObj["contacts"].([]interface{}) + if !ok || len(contacts) != 2 { + t.Fatalf("Expected contacts array with 2 items") + } + + // Validate first contact + contact1, ok := contacts[0].(map[string]interface{}) + if !ok { + t.Fatalf("Expected first contact to be an object") + } + + if email1, ok := contact1["email"].(string); !ok || email1 != "johndoe@anon.com" { + t.Errorf("First contact email should be johndoe@anon.com, got %v", contact1["email"]) + } + + if phone1, ok := contact1["phone"].(string); !ok || phone1 != "123-XXX-XXXX" { + t.Errorf("First contact phone should be 123-XXX-XXXX, got %v", contact1["phone"]) + } + + // Validate second contact + contact2, ok := contacts[1].(map[string]interface{}) + if !ok { + t.Fatalf("Expected second contact to be an object") + } + + if email2, ok := contact2["email"].(string); !ok || email2 != "janesmith@anon.com" { + t.Errorf("Second contact email should be janesmith@anon.com, got %v", contact2["email"]) + } + + if phone2, ok := contact2["phone"].(string); !ok || phone2 != "456-XXX-XXXX" { + t.Errorf("Second contact phone should be 456-XXX-XXXX, got %v", contact2["phone"]) + } +} + +// TestJSONProcessor_ChainedOperations tests sequential operations on the same data +func TestJSONProcessor_ChainedOperations(t *testing.T) { + content := `{ + "product": { + "name": "Widget", + "price": 100 + } + }` + + expected := `{ + "product": { + "name": "Widget", + "price": 103.5 + } + }` + + p := &JSONProcessor{} + result, modCount, matchCount, err := p.ProcessContent(content, "$.product.price", ` + -- When v is a numeric value, we can perform math operations directly + local price = v + -- Add 15% tax + price = price * 1.15 + -- Apply 10% discount + price = price * 0.9 + -- Round to 2 decimal places + price = math.floor(price * 100 + 0.5) / 100 + v = price + `) + + if err != nil { + t.Fatalf("Error processing content: %v", err) + } + + if matchCount != 1 { + t.Errorf("Expected 1 match, got %d", matchCount) + } + + if modCount != 1 { + t.Errorf("Expected 1 modification, got %d", modCount) + } + + // Normalize whitespace for comparison + normalizedResult := normalizeWhitespace(result) + normalizedExpected := normalizeWhitespace(expected) + + if normalizedResult != normalizedExpected { + t.Errorf("Expected content to be:\n%s\n\nGot:\n%s", expected, result) + } +} + +// TestJSONProcessor_ComplexDataTransformation tests advanced JSON transformation +func TestJSONProcessor_ComplexDataTransformation(t *testing.T) { + content := `{ + "store": { + "name": "My Store", + "inventory": [ + { + "id": 1, + "name": "Laptop", + "category": "electronics", + "price": 999.99, + "stock": 15, + "features": ["16GB RAM", "512GB SSD", "15-inch display"] + }, + { + "id": 2, + "name": "Smartphone", + "category": "electronics", + "price": 499.99, + "stock": 25, + "features": ["6GB RAM", "128GB storage", "5G"] + }, + { + "id": 3, + "name": "T-Shirt", + "category": "clothing", + "price": 19.99, + "stock": 100, + "features": ["100% cotton", "M, L, XL sizes", "Red color"] + }, + { + "id": 4, + "name": "Headphones", + "category": "electronics", + "price": 149.99, + "stock": 8, + "features": ["Noise cancelling", "Bluetooth", "20hr battery"] + } + ] + } + }` + + expected := `{ + "store": { + "name": "My Store", + "inventory_summary": { + "electronics": { + "count": 3, + "total_value": 30924.77, + "low_stock_items": [ + { + "id": 4, + "name": "Headphones", + "stock": 8 + } + ] + }, + "clothing": { + "count": 1, + "total_value": 1999.00, + "low_stock_items": [] + } + }, + "transformed_items": [ + { + "name": "Laptop", + "price_with_tax": 1199.99, + "in_stock": true + }, + { + "name": "Smartphone", + "price_with_tax": 599.99, + "in_stock": true + }, + { + "name": "T-Shirt", + "price_with_tax": 23.99, + "in_stock": true + }, + { + "name": "Headphones", + "price_with_tax": 179.99, + "in_stock": true + } + ] + } + }` + + p := &JSONProcessor{} + + // First, create a complex transformation that: + // 1. Summarizes inventory by category (count, total value, low stock alerts) + // 2. Creates a simplified view of items with tax added + result, modCount, matchCount, err := p.ProcessContent(content, "$", ` + -- Get store data + local store = v.store + local inventory = store.inventory + + -- Remove the original inventory array, we'll replace it with our summaries + store.inventory = nil + + -- Create summary by category + local summary = {} + local transformed = {} + + -- Group and analyze items by category + for _, item in ipairs(inventory) do + -- Prepare category data if not exists + local category = item.category + if not summary[category] then + summary[category] = { + count = 0, + total_value = 0, + low_stock_items = {} + } + end + + -- Update category counts + summary[category].count = summary[category].count + 1 + + -- Calculate total value (price * stock) and add to category + local item_value = item.price * item.stock + summary[category].total_value = summary[category].total_value + item_value + + -- Check for low stock (less than 10) + if item.stock < 10 then + table.insert(summary[category].low_stock_items, { + id = item.id, + name = item.name, + stock = item.stock + }) + end + + -- Create transformed view of the item with added tax + table.insert(transformed, { + name = item.name, + price_with_tax = math.floor((item.price * 1.2) * 100 + 0.5) / 100, -- 20% tax, rounded to 2 decimals + in_stock = item.stock > 0 + }) + end + + -- Format the total_value with two decimal places + for category, data in pairs(summary) do + data.total_value = math.floor(data.total_value * 100 + 0.5) / 100 + end + + -- Add our new data structures to the store + store.inventory_summary = summary + store.transformed_items = transformed + `) + + if err != nil { + t.Fatalf("Error processing content: %v", err) + } + + if matchCount != 1 { + t.Errorf("Expected 1 match, got %d", matchCount) + } + + if modCount != 1 { + t.Errorf("Expected 1 modification, got %d", modCount) + } + + // Parse both results as JSON objects for deep comparison + var resultObj map[string]interface{} + var expectedObj map[string]interface{} + + if err := json.Unmarshal([]byte(result), &resultObj); err != nil { + t.Fatalf("Failed to parse result JSON: %v", err) + } + + if err := json.Unmarshal([]byte(expected), &expectedObj); err != nil { + t.Fatalf("Failed to parse expected JSON: %v", err) + } + + // Verify the structure and key counts + resultStore, ok := resultObj["store"].(map[string]interface{}) + if !ok { + t.Fatalf("Expected 'store' object in result") + } + + // Check that inventory is gone and replaced with our new structures + if resultStore["inventory"] != nil { + t.Errorf("Expected 'inventory' to be removed") + } + + if resultStore["inventory_summary"] == nil { + t.Errorf("Expected 'inventory_summary' to be added") + } + + if resultStore["transformed_items"] == nil { + t.Errorf("Expected 'transformed_items' to be added") + } + + // Check that the transformed_items array has the correct length + transformedItems, ok := resultStore["transformed_items"].([]interface{}) + if !ok { + t.Fatalf("Expected 'transformed_items' to be an array") + } + + if len(transformedItems) != 4 { + t.Errorf("Expected 'transformed_items' to have 4 items, got %d", len(transformedItems)) + } + + // Check that the summary has entries for both electronics and clothing + summary, ok := resultStore["inventory_summary"].(map[string]interface{}) + if !ok { + t.Fatalf("Expected 'inventory_summary' to be an object") + } + + if summary["electronics"] == nil { + t.Errorf("Expected 'electronics' category in summary") + } + + if summary["clothing"] == nil { + t.Errorf("Expected 'clothing' category in summary") + } +}