package elasticsearch import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // TestFieldExtractor tests the field extraction utility func TestFieldExtractor(t *testing.T) { extractor := &fieldExtractor{} t.Run("getString", func(t *testing.T) { data := map[string]any{ "field": "value", "number": 42, "missing": nil, } assert.Equal(t, "value", extractor.getString(data, "field")) assert.Equal(t, "", extractor.getString(data, "number")) assert.Equal(t, "", extractor.getString(data, "missing")) assert.Equal(t, "", extractor.getString(data, "nonexistent")) }) t.Run("getInt", func(t *testing.T) { data := map[string]any{ "float": 42.0, "int": 100, "string": "200", "bad": "notanumber", } assert.Equal(t, 42, extractor.getInt(data, "float")) assert.Equal(t, 100, extractor.getInt(data, "int")) assert.Equal(t, 200, extractor.getInt(data, "string")) assert.Equal(t, 0, extractor.getInt(data, "bad")) assert.Equal(t, 0, extractor.getInt(data, "nonexistent")) }) t.Run("getFloat", func(t *testing.T) { data := map[string]any{ "float": 42.5, "int": 100, "string": "3.14", } assert.Equal(t, 42.5, extractor.getFloat(data, "float")) assert.Equal(t, 100.0, extractor.getFloat(data, "int")) assert.Equal(t, 3.14, extractor.getFloat(data, "string")) assert.Equal(t, 0.0, extractor.getFloat(data, "nonexistent")) }) t.Run("getMap", func(t *testing.T) { data := map[string]any{ "map": map[string]any{"key": "value"}, "notmap": "string", } result := extractor.getMap(data, "map") require.NotNil(t, result) assert.Equal(t, "value", result["key"]) assert.Nil(t, extractor.getMap(data, "notmap")) assert.Nil(t, extractor.getMap(data, "nonexistent")) }) } // TestDateHistogramParser tests the date histogram parser func TestDateHistogramParser(t *testing.T) { parser := &dateHistogramParser{extractor: &fieldExtractor{}} t.Run("CanParse", func(t *testing.T) { assert.True(t, parser.CanParse(dateHistType)) assert.False(t, parser.CanParse("terms")) }) t.Run("Parse with fixed_interval", func(t *testing.T) { aggValue := map[string]any{ "field": "@timestamp", "fixed_interval": "30s", "min_doc_count": 1, } agg, err := parser.Parse("1", dateHistType, aggValue) require.NoError(t, err) require.NotNil(t, agg) bucket := agg.toBucketAgg() assert.Equal(t, "1", bucket.ID) assert.Equal(t, dateHistType, bucket.Type) assert.Equal(t, "@timestamp", bucket.Field) assert.Equal(t, "30s", bucket.Settings.Get("interval").MustString()) assert.Equal(t, "1", bucket.Settings.Get("min_doc_count").MustString()) }) t.Run("Parse with calendar_interval", func(t *testing.T) { aggValue := map[string]any{ "field": "@timestamp", "calendar_interval": "1d", "time_zone": "UTC", } agg, err := parser.Parse("2", dateHistType, aggValue) require.NoError(t, err) require.NotNil(t, agg) bucket := agg.toBucketAgg() assert.Equal(t, "1d", bucket.Settings.Get("interval").MustString()) assert.Equal(t, "UTC", bucket.Settings.Get("time_zone").MustString()) }) t.Run("Parse returns bucket aggregation", func(t *testing.T) { agg, err := parser.Parse("1", dateHistType, map[string]any{"field": "@timestamp"}) assert.NoError(t, err) assert.NotNil(t, agg) assert.Equal(t, aggTypeBucket, agg.AggType) }) } // TestTermsParser tests the terms parser func TestTermsParser(t *testing.T) { parser := &termsParser{extractor: &fieldExtractor{}} t.Run("CanParse", func(t *testing.T) { assert.True(t, parser.CanParse(termsType)) assert.False(t, parser.CanParse("histogram")) }) t.Run("Parse", func(t *testing.T) { aggValue := map[string]any{ "field": "hostname.keyword", "size": 10, "order": map[string]any{"_count": "desc"}, } agg, err := parser.Parse("3", termsType, aggValue) require.NoError(t, err) require.NotNil(t, agg) bucket := agg.toBucketAgg() assert.Equal(t, "3", bucket.ID) assert.Equal(t, termsType, bucket.Type) assert.Equal(t, "hostname.keyword", bucket.Field) assert.Equal(t, "10", bucket.Settings.Get("size").MustString()) }) } // TestHistogramParser tests the histogram parser func TestHistogramParser(t *testing.T) { parser := &histogramParser{extractor: &fieldExtractor{}} t.Run("CanParse", func(t *testing.T) { assert.True(t, parser.CanParse(histogramType)) assert.False(t, parser.CanParse("terms")) }) t.Run("Parse", func(t *testing.T) { aggValue := map[string]any{ "field": "response_time", "interval": 50.0, } agg, err := parser.Parse("4", histogramType, aggValue) require.NoError(t, err) require.NotNil(t, agg) bucket := agg.toBucketAgg() assert.Equal(t, "4", bucket.ID) assert.Equal(t, histogramType, bucket.Type) assert.Equal(t, "response_time", bucket.Field) assert.Equal(t, "50", bucket.Settings.Get("interval").MustString()) }) } // TestFiltersParser tests the filters parser func TestFiltersParser(t *testing.T) { parser := &filtersParser{extractor: &fieldExtractor{}} t.Run("CanParse", func(t *testing.T) { assert.True(t, parser.CanParse(filtersType)) assert.False(t, parser.CanParse("terms")) }) t.Run("Parse", func(t *testing.T) { aggValue := map[string]any{ "filters": map[string]any{ "errors": map[string]any{"query_string": map[string]any{"query": "level:error"}}, "warnings": map[string]any{"query_string": map[string]any{"query": "level:warning"}}, }, } agg, err := parser.Parse("filters", filtersType, aggValue) require.NoError(t, err) require.NotNil(t, agg) bucket := agg.toBucketAgg() assert.Equal(t, "filters", bucket.ID) assert.Equal(t, filtersType, bucket.Type) filtersArray := bucket.Settings.Get("filters").MustArray() assert.NotEmpty(t, filtersArray) assert.Len(t, filtersArray, 2) }) } // TestSimpleMetricParser tests the simple metric parser func TestSimpleMetricParser(t *testing.T) { parser := newSimpleMetricParser() t.Run("CanParse", func(t *testing.T) { assert.True(t, parser.CanParse("avg")) assert.True(t, parser.CanParse("sum")) assert.True(t, parser.CanParse("min")) assert.True(t, parser.CanParse("max")) assert.True(t, parser.CanParse("cardinality")) assert.False(t, parser.CanParse("bucket_script")) }) t.Run("Parse avg", func(t *testing.T) { aggValue := map[string]any{ "field": "response_time", } agg, err := parser.Parse("1", "avg", aggValue) require.NoError(t, err) require.NotNil(t, agg) metric := agg.toMetricAgg() assert.Equal(t, "1", metric.ID) assert.Equal(t, "avg", metric.Type) assert.Equal(t, "response_time", metric.Field) }) t.Run("Parse returns metric aggregation", func(t *testing.T) { agg, err := parser.Parse("1", "avg", map[string]any{}) assert.NoError(t, err) assert.NotNil(t, agg) assert.Equal(t, aggTypeMetric, agg.AggType) }) } // TestExtendedStatsParser tests the extended stats parser func TestExtendedStatsParser(t *testing.T) { parser := &extendedStatsParser{extractor: &fieldExtractor{}} t.Run("CanParse", func(t *testing.T) { assert.True(t, parser.CanParse(extendedStatsType)) assert.False(t, parser.CanParse("avg")) }) t.Run("Parse", func(t *testing.T) { aggValue := map[string]any{ "field": "response_time", "sigma": 2, } agg, err := parser.Parse("stats", extendedStatsType, aggValue) require.NoError(t, err) require.NotNil(t, agg) metric := agg.toMetricAgg() assert.Equal(t, "stats", metric.ID) assert.Equal(t, extendedStatsType, metric.Type) assert.Equal(t, "response_time", metric.Field) }) } // TestPercentilesParser tests the percentiles parser func TestPercentilesParser(t *testing.T) { parser := &percentilesParser{extractor: &fieldExtractor{}} t.Run("CanParse", func(t *testing.T) { assert.True(t, parser.CanParse(percentilesType)) assert.False(t, parser.CanParse("avg")) }) t.Run("Parse", func(t *testing.T) { aggValue := map[string]any{ "field": "response_time", "percents": []any{50.0, 95.0, 99.0}, } agg, err := parser.Parse("percentiles", percentilesType, aggValue) require.NoError(t, err) require.NotNil(t, agg) metric := agg.toMetricAgg() assert.Equal(t, "percentiles", metric.ID) assert.Equal(t, percentilesType, metric.Type) assert.Equal(t, "response_time", metric.Field) }) } // TestPipelineParser tests the pipeline parser func TestPipelineParser(t *testing.T) { parser := newPipelineParser() t.Run("CanParse", func(t *testing.T) { assert.True(t, parser.CanParse("moving_avg")) assert.True(t, parser.CanParse("derivative")) assert.True(t, parser.CanParse("cumulative_sum")) assert.False(t, parser.CanParse("bucket_script")) }) t.Run("Parse", func(t *testing.T) { aggValue := map[string]any{ "buckets_path": "1", } agg, err := parser.Parse("moving", "moving_avg", aggValue) require.NoError(t, err) require.NotNil(t, agg) metric := agg.toMetricAgg() assert.Equal(t, "moving", metric.ID) assert.Equal(t, "moving_avg", metric.Type) assert.Equal(t, "1", metric.Field) }) } // TestBucketScriptParser tests the bucket script parser func TestBucketScriptParser(t *testing.T) { parser := &bucketScriptParser{extractor: &fieldExtractor{}} t.Run("CanParse", func(t *testing.T) { assert.True(t, parser.CanParse("bucket_script")) assert.False(t, parser.CanParse("moving_avg")) }) t.Run("Parse with map buckets_path", func(t *testing.T) { aggValue := map[string]any{ "buckets_path": map[string]any{ "count": "total", }, "script": "params.count / 60", } agg, err := parser.Parse("rate", "bucket_script", aggValue) require.NoError(t, err) require.NotNil(t, agg) metric := agg.toMetricAgg() assert.Equal(t, "rate", metric.ID) assert.Equal(t, "bucket_script", metric.Type) assert.Equal(t, "total", metric.PipelineVariables["count"]) assert.Equal(t, "params.count / 60", metric.Settings.Get("script").MustString()) }) t.Run("Parse with string buckets_path", func(t *testing.T) { aggValue := map[string]any{ "buckets_path": "1", } agg, err := parser.Parse("rate", "bucket_script", aggValue) require.NoError(t, err) require.NotNil(t, agg) metric := agg.toMetricAgg() assert.Equal(t, "1", metric.PipelineVariables["var1"]) }) } // TestCompositeParser tests the full parser integration func TestCompositeParser(t *testing.T) { parser := NewAggregationParser() t.Run("Parse date histogram aggregation", func(t *testing.T) { rawQuery := `{ "query": { "match_all": {} }, "aggs": { "2": { "date_histogram": { "field": "@timestamp", "fixed_interval": "30s", "min_doc_count": 1 } } } }` bucketAggs, metricAggs, err := parser.Parse(rawQuery) require.NoError(t, err) require.Len(t, bucketAggs, 1) require.Len(t, metricAggs, 0) assert.Equal(t, "2", bucketAggs[0].ID) assert.Equal(t, dateHistType, bucketAggs[0].Type) assert.Equal(t, "@timestamp", bucketAggs[0].Field) assert.Equal(t, "30s", bucketAggs[0].Settings.Get("interval").MustString()) }) t.Run("Parse nested aggregations with metrics", func(t *testing.T) { rawQuery := `{ "query": { "match_all": {} }, "aggs": { "2": { "date_histogram": { "field": "@timestamp", "fixed_interval": "30s" }, "aggs": { "1": { "avg": { "field": "value" } }, "3": { "sum": { "field": "total" } } } } } }` bucketAggs, metricAggs, err := parser.Parse(rawQuery) require.NoError(t, err) require.Len(t, bucketAggs, 1) require.Len(t, metricAggs, 2) // Check bucket aggregation assert.Equal(t, "2", bucketAggs[0].ID) assert.Equal(t, dateHistType, bucketAggs[0].Type) // Check metric aggregations avgFound := false sumFound := false for _, m := range metricAggs { if m.ID == "1" && m.Type == "avg" && m.Field == "value" { avgFound = true } if m.ID == "3" && m.Type == "sum" && m.Field == "total" { sumFound = true } } assert.True(t, avgFound, "avg aggregation not found") assert.True(t, sumFound, "sum aggregation not found") }) t.Run("Parse terms aggregation", func(t *testing.T) { rawQuery := `{ "aggs": { "3": { "terms": { "field": "hostname.keyword", "size": 10, "order": { "_count": "desc" } } } } }` bucketAggs, metricAggs, err := parser.Parse(rawQuery) require.NoError(t, err) require.Len(t, bucketAggs, 1) require.Len(t, metricAggs, 0) assert.Equal(t, "3", bucketAggs[0].ID) assert.Equal(t, termsType, bucketAggs[0].Type) assert.Equal(t, "hostname.keyword", bucketAggs[0].Field) assert.Equal(t, "10", bucketAggs[0].Settings.Get("size").MustString()) }) t.Run("Parse histogram aggregation", func(t *testing.T) { rawQuery := `{ "aggs": { "4": { "histogram": { "field": "response_time", "interval": 50 } } } }` bucketAggs, _, err := parser.Parse(rawQuery) require.NoError(t, err) require.Len(t, bucketAggs, 1) assert.Equal(t, "4", bucketAggs[0].ID) assert.Equal(t, histogramType, bucketAggs[0].Type) assert.Equal(t, "response_time", bucketAggs[0].Field) assert.Equal(t, "50", bucketAggs[0].Settings.Get("interval").MustString()) }) t.Run("Parse extended stats aggregation", func(t *testing.T) { rawQuery := `{ "aggs": { "stats": { "extended_stats": { "field": "response_time", "sigma": 2 } } } }` _, metricAggs, err := parser.Parse(rawQuery) require.NoError(t, err) require.Len(t, metricAggs, 1) assert.Equal(t, "stats", metricAggs[0].ID) assert.Equal(t, extendedStatsType, metricAggs[0].Type) assert.Equal(t, "response_time", metricAggs[0].Field) }) t.Run("Parse percentiles aggregation", func(t *testing.T) { rawQuery := `{ "aggs": { "percentiles": { "percentiles": { "field": "response_time", "percents": [50, 95, 99] } } } }` _, metricAggs, err := parser.Parse(rawQuery) require.NoError(t, err) require.Len(t, metricAggs, 1) assert.Equal(t, "percentiles", metricAggs[0].ID) assert.Equal(t, percentilesType, metricAggs[0].Type) }) t.Run("Parse pipeline aggregations", func(t *testing.T) { rawQuery := `{ "aggs": { "2": { "date_histogram": { "field": "@timestamp", "fixed_interval": "1m" }, "aggs": { "1": { "avg": { "field": "value" } }, "moving": { "moving_avg": { "buckets_path": "1" } }, "deriv": { "derivative": { "buckets_path": "1" } } } } } }` bucketAggs, metricAggs, err := parser.Parse(rawQuery) require.NoError(t, err) require.Len(t, bucketAggs, 1) require.GreaterOrEqual(t, len(metricAggs), 2) // At least avg and one pipeline // Find pipeline aggregations movingAvgFound := false derivativeFound := false for _, m := range metricAggs { if m.ID == "moving" && m.Type == "moving_avg" { movingAvgFound = true assert.Equal(t, "1", m.Field) } if m.ID == "deriv" && m.Type == "derivative" { derivativeFound = true assert.Equal(t, "1", m.Field) } } assert.True(t, movingAvgFound, "moving_avg aggregation not found") assert.True(t, derivativeFound, "derivative aggregation not found") }) t.Run("Parse bucket script aggregation", func(t *testing.T) { rawQuery := `{ "aggs": { "2": { "date_histogram": { "field": "@timestamp", "fixed_interval": "1m" }, "aggs": { "total": { "sum": { "field": "bytes" } }, "rate": { "bucket_script": { "buckets_path": { "count": "total" }, "script": "params.count / 60" } } } } } }` _, metricAggs, err := parser.Parse(rawQuery) require.NoError(t, err) // Find bucket script var bucketScriptAgg *MetricAgg for _, m := range metricAggs { if m.ID == "rate" && m.Type == "bucket_script" { bucketScriptAgg = m break } } require.NotNil(t, bucketScriptAgg, "bucket_script aggregation not found") assert.Equal(t, "params.count / 60", bucketScriptAgg.Settings.Get("script").MustString()) assert.Equal(t, "total", bucketScriptAgg.PipelineVariables["count"]) }) t.Run("Parse filters aggregation", func(t *testing.T) { rawQuery := `{ "aggs": { "messages": { "filters": { "filters": { "errors": { "query_string": { "query": "level:error" } }, "warnings": { "query_string": { "query": "level:warning" } } } } } } }` bucketAggs, _, err := parser.Parse(rawQuery) require.NoError(t, err) require.Len(t, bucketAggs, 1) assert.Equal(t, "messages", bucketAggs[0].ID) assert.Equal(t, filtersType, bucketAggs[0].Type) }) t.Run("Handle empty query", func(t *testing.T) { bucketAggs, metricAggs, err := parser.Parse("") require.NoError(t, err) assert.Nil(t, bucketAggs) assert.Nil(t, metricAggs) }) t.Run("Handle query without aggregations", func(t *testing.T) { rawQuery := `{ "query": { "match_all": {} } }` bucketAggs, metricAggs, err := parser.Parse(rawQuery) require.NoError(t, err) assert.Nil(t, bucketAggs) assert.Nil(t, metricAggs) }) t.Run("Handle invalid JSON", func(t *testing.T) { rawQuery := `{invalid json` _, _, err := parser.Parse(rawQuery) require.Error(t, err) }) t.Run("Use 'aggregations' instead of 'aggs'", func(t *testing.T) { rawQuery := `{ "query": { "match_all": {} }, "aggregations": { "2": { "date_histogram": { "field": "@timestamp", "fixed_interval": "30s" } } } }` bucketAggs, _, err := parser.Parse(rawQuery) require.NoError(t, err) require.Len(t, bucketAggs, 1) assert.Equal(t, "2", bucketAggs[0].ID) }) }