Add more json tests

To bring it in line with the xml ones
This commit is contained in:
2025-03-25 19:20:42 +01:00
parent 872f2dd46d
commit 68127fe453

View File

@@ -1096,3 +1096,676 @@ func TestJSONProcessor_RootNodeModification(t *testing.T) {
t.Errorf("Expected content to be:\n%s\n\nGot:\n%s", expected, result) 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")
}
}