1+ // Copyright (c) 2025 The Jaeger Authors.
2+ // SPDX-License-Identifier: Apache-2.0
3+
4+ package query
5+
6+ import (
7+ "testing"
8+
9+ "github.com/olivere/elastic/v7"
10+ "github.com/stretchr/testify/assert"
11+ "github.com/stretchr/testify/require"
12+
13+ "github.com/jaegertracing/jaeger/internal/storage/elasticsearch/dbmodel"
14+ )
15+
16+ func TestNewTagQueryBuilder (t * testing.T ) {
17+ dotReplacer := dbmodel .NewDotReplacer ("@" )
18+ builder := NewTagQueryBuilder (dotReplacer )
19+
20+ assert .NotNil (t , builder )
21+ assert .Equal (t , dotReplacer , builder .dotReplacer )
22+ }
23+
24+ func TestTagQueryBuilder_BuildTagQuery (t * testing.T ) {
25+ tests := []struct {
26+ name string
27+ key string
28+ value string
29+ dotReplacement string
30+ expectedQueries int
31+ }{
32+ {
33+ name : "simple key-value" ,
34+ key : "environment" ,
35+ value : "production" ,
36+ dotReplacement : "@" ,
37+ expectedQueries : 5 , // 2 object queries + 3 nested queries
38+ },
39+ {
40+ name : "key with dots" ,
41+ key : "service.name" ,
42+ value : "user-service" ,
43+ dotReplacement : "@" ,
44+ expectedQueries : 5 ,
45+ },
46+ {
47+ name : "special characters in value" ,
48+ key : "version" ,
49+ value : "v1.2.3" ,
50+ dotReplacement : "#" ,
51+ expectedQueries : 5 ,
52+ },
53+ {
54+ name : "empty value" ,
55+ key : "status" ,
56+ value : "" ,
57+ dotReplacement : "@" ,
58+ expectedQueries : 5 ,
59+ },
60+ }
61+
62+ for _ , tt := range tests {
63+ t .Run (tt .name , func (t * testing.T ) {
64+ dotReplacer := dbmodel .NewDotReplacer (tt .dotReplacement )
65+ builder := NewTagQueryBuilder (dotReplacer )
66+
67+ query := builder .BuildTagQuery (tt .key , tt .value )
68+
69+ // Assert that we get a BoolQuery
70+ boolQuery , ok := query .(* elastic.BoolQuery )
71+ require .True (t , ok , "Expected BoolQuery" )
72+
73+ // Convert to map to inspect structure
74+ queryMap , err := boolQuery .Source ()
75+ require .NoError (t , err )
76+
77+ // Check that it's a bool query with should clause
78+ queryMapTyped , ok := queryMap .(map [string ]interface {})
79+ require .True (t , ok , "Expected query map to be map[string]interface{}" )
80+
81+ boolClause , exists := queryMapTyped ["bool" ]
82+ require .True (t , exists , "Expected bool clause" )
83+
84+ boolMap , ok := boolClause .(map [string ]interface {})
85+ require .True (t , ok , "Expected bool clause to be map" )
86+
87+ shouldClause , exists := boolMap ["should" ]
88+ require .True (t , exists , "Expected should clause" )
89+
90+ shouldQueries , ok := shouldClause .([]interface {})
91+ require .True (t , ok , "Expected should clause to be array" )
92+
93+ // Verify we have the expected number of queries
94+ assert .Equal (t , tt .expectedQueries , len (shouldQueries ), "Expected %d queries" , tt .expectedQueries )
95+ })
96+ }
97+ }
98+
99+ func TestTagQueryBuilder_BuildNestedQuery (t * testing.T ) {
100+ tests := []struct {
101+ name string
102+ field string
103+ key string
104+ value string
105+ expected map [string ]interface {}
106+ }{
107+ {
108+ name : "nested tags query" ,
109+ field : "tags" ,
110+ key : "environment" ,
111+ value : "production" ,
112+ },
113+ {
114+ name : "nested process tags query" ,
115+ field : "process.tags" ,
116+ key : "service.name" ,
117+ value : "user-service" ,
118+ },
119+ {
120+ name : "nested log fields query" ,
121+ field : "logs.fields" ,
122+ key : "level" ,
123+ value : "error" ,
124+ },
125+ }
126+
127+ for _ , tt := range tests {
128+ t .Run (tt .name , func (t * testing.T ) {
129+ dotReplacer := dbmodel .NewDotReplacer ("@" )
130+ builder := NewTagQueryBuilder (dotReplacer )
131+
132+ query := builder .buildNestedQuery (tt .field , tt .key , tt .value )
133+
134+ // Assert that we get a NestedQuery
135+ nestedQuery , ok := query .(* elastic.NestedQuery )
136+ require .True (t , ok , "Expected NestedQuery" )
137+
138+ // Convert to map to inspect structure
139+ queryMap , err := nestedQuery .Source ()
140+ require .NoError (t , err )
141+
142+ // Check nested structure
143+ queryMapTyped , ok := queryMap .(map [string ]interface {})
144+ require .True (t , ok , "Expected query map to be map[string]interface{}" )
145+
146+ nestedClause , exists := queryMapTyped ["nested" ]
147+ require .True (t , exists , "Expected nested clause" )
148+
149+ nestedMap , ok := nestedClause .(map [string ]interface {})
150+ require .True (t , ok , "Expected nested clause to be map" )
151+
152+ // Check path
153+ path , exists := nestedMap ["path" ]
154+ require .True (t , exists , "Expected path in nested query" )
155+ assert .Equal (t , tt .field , path , "Expected path to match field" )
156+
157+ // Check query structure
158+ queryClause , exists := nestedMap ["query" ]
159+ require .True (t , exists , "Expected query in nested clause" )
160+
161+ queryBool , ok := queryClause .(map [string ]interface {})
162+ require .True (t , ok , "Expected query to be map" )
163+
164+ // Should have bool -> must structure
165+ boolClause , exists := queryBool ["bool" ]
166+ require .True (t , exists , "Expected bool clause in nested query" )
167+
168+ boolMap , ok := boolClause .(map [string ]interface {})
169+ require .True (t , ok , "Expected bool clause to be map" )
170+
171+ mustClause , exists := boolMap ["must" ]
172+ require .True (t , exists , "Expected must clause" )
173+
174+ mustQueries , ok := mustClause .([]interface {})
175+ require .True (t , ok , "Expected must clause to be array" )
176+
177+ // Should have exactly 2 queries: key match and value regexp
178+ assert .Equal (t , 2 , len (mustQueries ), "Expected 2 must queries" )
179+ })
180+ }
181+ }
182+
183+ func TestTagQueryBuilder_BuildObjectQuery (t * testing.T ) {
184+ tests := []struct {
185+ name string
186+ field string
187+ key string
188+ value string
189+ }{
190+ {
191+ name : "object tag query" ,
192+ field : "tag" ,
193+ key : "environment" ,
194+ value : "production" ,
195+ },
196+ {
197+ name : "object process tag query" ,
198+ field : "process.tag" ,
199+ key : "service@name" , // Already dot-replaced
200+ value : "user-service" ,
201+ },
202+ }
203+
204+ for _ , tt := range tests {
205+ t .Run (tt .name , func (t * testing.T ) {
206+ dotReplacer := dbmodel .NewDotReplacer ("@" )
207+ builder := NewTagQueryBuilder (dotReplacer )
208+
209+ query := builder .buildObjectQuery (tt .field , tt .key , tt .value )
210+
211+ // Assert that we get a BoolQuery
212+ boolQuery , ok := query .(* elastic.BoolQuery )
213+ require .True (t , ok , "Expected BoolQuery" )
214+
215+ // Convert to map to inspect structure
216+ queryMap , err := boolQuery .Source ()
217+ require .NoError (t , err )
218+
219+ // Check bool structure
220+ queryMapTyped , ok := queryMap .(map [string ]interface {})
221+ require .True (t , ok , "Expected query map to be map[string]interface{}" )
222+
223+ boolClause , exists := queryMapTyped ["bool" ]
224+ require .True (t , exists , "Expected bool clause" )
225+
226+ boolMap , ok := boolClause .(map [string ]interface {})
227+ require .True (t , ok , "Expected bool clause to be map" )
228+
229+ mustClause , exists := boolMap ["must" ]
230+ require .True (t , exists , "Expected must clause" )
231+
232+ // Must clause can be either a single query or an array of queries
233+ // When there's only one query, elastic returns it as a single object
234+ // When there are multiple queries, it returns an array
235+ if mustArray , ok := mustClause .([]interface {}); ok {
236+ // Multiple queries case
237+ assert .Equal (t , 1 , len (mustArray ), "Expected 1 must query" )
238+
239+ // Check that it's a regexp query
240+ regexpQuery , ok := mustArray [0 ].(map [string ]interface {})
241+ require .True (t , ok , "Expected query to be map" )
242+
243+ _ , exists = regexpQuery ["regexp" ]
244+ assert .True (t , exists , "Expected regexp query" )
245+ } else if mustQuery , ok := mustClause .(map [string ]interface {}); ok {
246+ // Single query case
247+ _ , exists = mustQuery ["regexp" ]
248+ assert .True (t , exists , "Expected regexp query" )
249+ } else {
250+ t .Fatal ("Expected must clause to be either array or single query" )
251+ }
252+ })
253+ }
254+ }
255+
256+ func TestTagQueryBuilder_DotReplacement (t * testing.T ) {
257+ tests := []struct {
258+ name string
259+ dotReplacement string
260+ key string
261+ expectedKey string
262+ }{
263+ {
264+ name : "replace dots with @" ,
265+ dotReplacement : "@" ,
266+ key : "service.name" ,
267+ expectedKey : "service@name" ,
268+ },
269+ {
270+ name : "replace dots with #" ,
271+ dotReplacement : "#" ,
272+ key : "trace.span.id" ,
273+ expectedKey : "trace#span#id" ,
274+ },
275+ {
276+ name : "no dots in key" ,
277+ dotReplacement : "@" ,
278+ key : "environment" ,
279+ expectedKey : "environment" ,
280+ },
281+ {
282+ name : "multiple dots" ,
283+ dotReplacement : "_" ,
284+ key : "a.b.c.d" ,
285+ expectedKey : "a_b_c_d" ,
286+ },
287+ }
288+
289+ for _ , tt := range tests {
290+ t .Run (tt .name , func (t * testing.T ) {
291+ dotReplacer := dbmodel .NewDotReplacer (tt .dotReplacement )
292+ builder := NewTagQueryBuilder (dotReplacer )
293+
294+ // Build a query and check that dot replacement is applied in object queries
295+ query := builder .BuildTagQuery (tt .key , "test-value" )
296+
297+ // Convert to map to inspect
298+ boolQuery , ok := query .(* elastic.BoolQuery )
299+ require .True (t , ok , "Expected BoolQuery" )
300+
301+ queryMap , err := boolQuery .Source ()
302+ require .NoError (t , err )
303+
304+ // The dot replacement should be visible in the object queries
305+ // We can verify this by checking the structure contains the replaced key
306+ queryMapTyped , ok := queryMap .(map [string ]interface {})
307+ require .True (t , ok , "Expected query map to be map[string]interface{}" )
308+
309+ boolClause := queryMapTyped ["bool" ].(map [string ]interface {})
310+ shouldClause := boolClause ["should" ].([]interface {})
311+
312+ // First two queries should be object queries with dot replacement
313+ // This is a basic structural test - more detailed testing would require
314+ // examining the specific field names in the regexp queries
315+ assert .Equal (t , 5 , len (shouldClause ), "Expected 5 should queries" )
316+ })
317+ }
318+ }
319+
320+ func TestTagQueryBuilder_EdgeCases (t * testing.T ) {
321+ dotReplacer := dbmodel .NewDotReplacer ("@" )
322+ builder := NewTagQueryBuilder (dotReplacer )
323+
324+ t .Run ("empty key" , func (t * testing.T ) {
325+ query := builder .BuildTagQuery ("" , "value" )
326+ assert .NotNil (t , query )
327+
328+ // Should still build valid query structure
329+ boolQuery , ok := query .(* elastic.BoolQuery )
330+ require .True (t , ok , "Expected BoolQuery" )
331+
332+ queryMap , err := boolQuery .Source ()
333+ require .NoError (t , err )
334+
335+ queryMapTyped , ok := queryMap .(map [string ]interface {})
336+ require .True (t , ok , "Expected query map to be map[string]interface{}" )
337+ assert .Contains (t , queryMapTyped , "bool" )
338+ })
339+
340+ t .Run ("empty value" , func (t * testing.T ) {
341+ query := builder .BuildTagQuery ("key" , "" )
342+ assert .NotNil (t , query )
343+
344+ boolQuery , ok := query .(* elastic.BoolQuery )
345+ require .True (t , ok , "Expected BoolQuery" )
346+
347+ queryMap , err := boolQuery .Source ()
348+ require .NoError (t , err )
349+
350+ queryMapTyped , ok := queryMap .(map [string ]interface {})
351+ require .True (t , ok , "Expected query map to be map[string]interface{}" )
352+ assert .Contains (t , queryMapTyped , "bool" )
353+ })
354+
355+ t .Run ("special characters" , func (t * testing.T ) {
356+ specialKey := "key!@#$%^&*()"
357+ specialValue := "value!@#$%^&*()"
358+
359+ query := builder .BuildTagQuery (specialKey , specialValue )
360+ assert .NotNil (t , query )
361+
362+ boolQuery , ok := query .(* elastic.BoolQuery )
363+ require .True (t , ok , "Expected BoolQuery" )
364+
365+ queryMap , err := boolQuery .Source ()
366+ require .NoError (t , err )
367+
368+ queryMapTyped , ok := queryMap .(map [string ]interface {})
369+ require .True (t , ok , "Expected query map to be map[string]interface{}" )
370+ assert .Contains (t , queryMapTyped , "bool" )
371+ })
372+ }
373+
374+ func TestTagQueryBuilder_Constants (t * testing.T ) {
375+ // Test that constants are properly defined
376+ assert .Equal (t , "tag" , objectTagsField )
377+ assert .Equal (t , "process.tag" , objectProcessTagsField )
378+ assert .Equal (t , "tags" , nestedTagsField )
379+ assert .Equal (t , "process.tags" , nestedProcessTagsField )
380+ assert .Equal (t , "logs.fields" , nestedLogFieldsField )
381+ assert .Equal (t , "key" , tagKeyField )
382+ assert .Equal (t , "value" , tagValueField )
383+
384+ // Test field lists
385+ expectedObjectFields := []string {"tag" , "process.tag" }
386+ assert .Equal (t , expectedObjectFields , objectTagFieldList )
387+
388+ expectedNestedFields := []string {"tags" , "process.tags" , "logs.fields" }
389+ assert .Equal (t , expectedNestedFields , nestedTagFieldList )
390+ }
0 commit comments