15
15
package apiv3
16
16
17
17
import (
18
+ "bytes"
18
19
"context"
19
20
"encoding/json"
20
21
"fmt"
21
22
"io"
22
23
"net"
23
24
"net/http"
25
+ "net/url"
26
+ "os"
27
+ "path/filepath"
24
28
"strings"
25
29
"testing"
30
+ "time"
26
31
32
+ gogojsonpb "github.com/gogo/protobuf/jsonpb"
33
+ gogoproto "github.com/gogo/protobuf/proto"
27
34
"github.com/gorilla/mux"
28
- "github.com/grpc-ecosystem/grpc-gateway/runtime"
29
35
"github.com/stretchr/testify/assert"
36
+ "github.com/stretchr/testify/mock"
30
37
"github.com/stretchr/testify/require"
31
38
"go.uber.org/zap"
32
39
"google.golang.org/grpc"
@@ -39,23 +46,36 @@ import (
39
46
"github.com/jaegertracing/jaeger/pkg/tenancy"
40
47
"github.com/jaegertracing/jaeger/proto-gen/api_v3"
41
48
dependencyStoreMocks "github.com/jaegertracing/jaeger/storage/dependencystore/mocks"
49
+ "github.com/jaegertracing/jaeger/storage/spanstore"
42
50
spanstoremocks "github.com/jaegertracing/jaeger/storage/spanstore/mocks"
43
51
)
44
52
45
- var testCertKeyLocation = "../../../../pkg/config/tlscfg/testdata/"
53
+ const (
54
+ testCertKeyLocation = "../../../../pkg/config/tlscfg/testdata/"
55
+ snapshotLocation = "./snapshots/"
56
+ )
57
+
58
+ // Snapshots can be regenerated via:
59
+ //
60
+ // REGENERATE_SNAPSHOTS=true go test -v ./cmd/query/app/apiv3/...
61
+ var regenerateSnapshots = os .Getenv ("REGENERATE_SNAPSHOTS" ) == "true"
46
62
47
63
type testGateway struct {
48
64
reader * spanstoremocks.Reader
49
65
url string
50
66
}
51
67
68
+ type gatewayRequest struct {
69
+ url string
70
+ setupRequest func (* http.Request )
71
+ }
72
+
52
73
func setupGRPCGateway (
53
74
t * testing.T ,
54
75
basePath string ,
55
76
serverTLS , clientTLS * tlscfg.Options ,
56
77
tenancyOptions tenancy.Options ,
57
78
) * testGateway {
58
- // *spanstoremocks.Reader, net.Listener, *grpc.Server, context.CancelFunc, *http.Server
59
79
gw := & testGateway {
60
80
reader : & spanstoremocks.Reader {},
61
81
}
@@ -123,6 +143,64 @@ func setupGRPCGateway(
123
143
return gw
124
144
}
125
145
146
+ func (gw * testGateway ) execRequest (t * testing.T , gwReq * gatewayRequest ) ([]byte , int ) {
147
+ req , err := http .NewRequest (http .MethodGet , gw .url + gwReq .url , nil )
148
+ require .NoError (t , err )
149
+ req .Header .Set ("Content-Type" , "application/json" )
150
+ gwReq .setupRequest (req )
151
+ response , err := http .DefaultClient .Do (req )
152
+ require .NoError (t , err )
153
+ body , err := io .ReadAll (response .Body )
154
+ require .NoError (t , err )
155
+ require .NoError (t , response .Body .Close ())
156
+ return body , response .StatusCode
157
+ }
158
+
159
+ func verifySnapshot (t * testing.T , body []byte ) []byte {
160
+ // reformat JSON body with indentation, to make diffing easier
161
+ var data interface {}
162
+ require .NoError (t , json .Unmarshal (body , & data ))
163
+ body , err := json .MarshalIndent (data , "" , " " )
164
+ require .NoError (t , err )
165
+
166
+ snapshotFile := filepath .Join (snapshotLocation , strings .ReplaceAll (t .Name (), "/" , "_" )+ ".json" )
167
+ if regenerateSnapshots {
168
+ os .WriteFile (snapshotFile , body , 0o644 )
169
+ }
170
+ snapshot , err := os .ReadFile (snapshotFile )
171
+ require .NoError (t , err )
172
+ assert .Equal (t , string (snapshot ), string (body ), "comparing against stored snapshot. Use REGENERATE_SNAPSHOTS=true to rebuild snapshots." )
173
+ return body
174
+ }
175
+
176
+ func parseResponse (t * testing.T , body []byte , obj gogoproto.Message ) {
177
+ require .NoError (t , gogojsonpb .Unmarshal (bytes .NewBuffer (body ), obj ))
178
+ }
179
+
180
+ func parseChunkResponse (t * testing.T , body []byte , obj gogoproto.Message ) {
181
+ // Unwrap the 'result' container generated by the gateway.
182
+ // See https://github.com/grpc-ecosystem/grpc-gateway/issues/2189
183
+ type resultWrapper struct {
184
+ Result json.RawMessage `json:"result"`
185
+ }
186
+ var result resultWrapper
187
+ require .NoError (t , json .Unmarshal (body , & result ))
188
+ parseResponse (t , result .Result , obj )
189
+ }
190
+
191
+ func makeTestTrace () (* model.Trace , model.TraceID ) {
192
+ traceID := model .NewTraceID (150 , 160 )
193
+ return & model.Trace {
194
+ Spans : []* model.Span {
195
+ {
196
+ TraceID : traceID ,
197
+ SpanID : model .NewSpanID (180 ),
198
+ OperationName : "foobar" ,
199
+ },
200
+ },
201
+ }, traceID
202
+ }
203
+
126
204
func testGRPCGateway (
127
205
t * testing.T , basePath string ,
128
206
serverTLS , clientTLS * tlscfg.Options ,
@@ -144,36 +222,94 @@ func testGRPCGatewayWithTenancy(
144
222
setupRequest func (* http.Request ),
145
223
) {
146
224
gw := setupGRPCGateway (t , basePath , serverTLS , clientTLS , tenancyOptions )
225
+ t .Run ("GetServices" , func (t * testing.T ) {
226
+ runGatewayGetServices (t , gw , setupRequest )
227
+ })
228
+ t .Run ("GetOperations" , func (t * testing.T ) {
229
+ runGatewayGetOperations (t , gw , setupRequest )
230
+ })
231
+ t .Run ("GetTrace" , func (t * testing.T ) {
232
+ runGatewayGetTrace (t , gw , setupRequest )
233
+ })
234
+ t .Run ("FindTraces" , func (t * testing.T ) {
235
+ runGatewayFindTraces (t , gw , setupRequest )
236
+ })
237
+ }
147
238
148
- traceID := model .NewTraceID (150 , 160 )
149
- gw .reader .On ("GetTrace" , matchContext , matchTraceID ).Return (
150
- & model.Trace {
151
- Spans : []* model.Span {
152
- {
153
- TraceID : traceID ,
154
- SpanID : model .NewSpanID (180 ),
155
- OperationName : "foobar" ,
156
- },
157
- },
158
- }, nil ).Once ()
239
+ func runGatewayGetServices (t * testing.T , gw * testGateway , setupRequest func (* http.Request )) {
240
+ gw .reader .On ("GetServices" , matchContext ).Return ([]string {"foo" }, nil ).Once ()
159
241
160
- req , err := http .NewRequest (http .MethodGet , gw .url + "/api/v3/traces/123" , nil )
161
- require .NoError (t , err )
162
- req .Header .Set ("Content-Type" , "application/json" )
163
- setupRequest (req )
164
- response , err := http .DefaultClient .Do (req )
165
- require .NoError (t , err )
166
- body , err := io .ReadAll (response .Body )
167
- require .NoError (t , err )
168
- require .NoError (t , response .Body .Close ())
242
+ body , statusCode := gw .execRequest (t , & gatewayRequest {
243
+ url : "/api/v3/services" ,
244
+ setupRequest : setupRequest ,
245
+ })
246
+ require .Equal (t , http .StatusOK , statusCode )
247
+ body = verifySnapshot (t , body )
248
+
249
+ var response api_v3.GetServicesResponse
250
+ parseResponse (t , body , & response )
251
+ assert .Equal (t , []string {"foo" }, response .Services )
252
+ }
253
+
254
+ func runGatewayGetOperations (t * testing.T , gw * testGateway , setupRequest func (* http.Request )) {
255
+ qp := spanstore.OperationQueryParameters {ServiceName : "foo" , SpanKind : "server" }
256
+ gw .reader .
257
+ On ("GetOperations" , matchContext , qp ).
258
+ Return ([]spanstore.Operation {{Name : "get_users" , SpanKind : "server" }}, nil ).Once ()
259
+
260
+ body , statusCode := gw .execRequest (t , & gatewayRequest {
261
+ url : "/api/v3/operations?service=foo&span_kind=server" ,
262
+ setupRequest : setupRequest ,
263
+ })
264
+ require .Equal (t , http .StatusOK , statusCode )
265
+ body = verifySnapshot (t , body )
266
+
267
+ var response api_v3.GetOperationsResponse
268
+ parseResponse (t , body , & response )
269
+ require .Len (t , response .Operations , 1 )
270
+ assert .Equal (t , "get_users" , response .Operations [0 ].Name )
271
+ assert .Equal (t , "server" , response .Operations [0 ].SpanKind )
272
+ }
273
+
274
+ func runGatewayGetTrace (t * testing.T , gw * testGateway , setupRequest func (* http.Request )) {
275
+ trace , traceID := makeTestTrace ()
276
+ gw .reader .On ("GetTrace" , matchContext , traceID ).Return (trace , nil ).Once ()
277
+
278
+ body , statusCode := gw .execRequest (t , & gatewayRequest {
279
+ url : "/api/v3/traces/" + traceID .String (), // hex string
280
+ setupRequest : setupRequest ,
281
+ })
282
+ require .Equal (t , http .StatusOK , statusCode , "response=%s" , string (body ))
283
+ body = verifySnapshot (t , body )
169
284
170
- jsonpb := & runtime.JSONPb {}
171
- var envelope envelope
172
- err = json .Unmarshal (body , & envelope )
173
- require .NoError (t , err )
174
285
var spansResponse api_v3.SpansResponseChunk
175
- err = jsonpb .Unmarshal (envelope .Result , & spansResponse )
176
- require .NoError (t , err )
286
+ parseChunkResponse (t , body , & spansResponse )
287
+
288
+ assert .Len (t , spansResponse .GetResourceSpans (), 1 )
289
+ assert .Equal (t , bytesOfTraceID (t , traceID .High , traceID .Low ), spansResponse .GetResourceSpans ()[0 ].GetScopeSpans ()[0 ].GetSpans ()[0 ].GetTraceId ())
290
+ }
291
+
292
+ func runGatewayFindTraces (t * testing.T , gw * testGateway , setupRequest func (* http.Request )) {
293
+ trace , traceID := makeTestTrace ()
294
+ gw .reader .
295
+ On ("FindTraces" , matchContext , mock .AnythingOfType ("*spanstore.TraceQueryParameters" )).
296
+ Return ([]* model.Trace {trace }, nil ).Once ()
297
+
298
+ q := url.Values {}
299
+ q .Set ("query.service_name" , "foobar" )
300
+ q .Set ("query.start_time_min" , time .Now ().Format (time .RFC3339 ))
301
+ q .Set ("query.start_time_max" , time .Now ().Format (time .RFC3339 ))
302
+
303
+ body , statusCode := gw .execRequest (t , & gatewayRequest {
304
+ url : "/api/v3/traces?" + q .Encode (),
305
+ setupRequest : setupRequest ,
306
+ })
307
+ require .Equal (t , http .StatusOK , statusCode , "response=%s" , string (body ))
308
+ body = verifySnapshot (t , body )
309
+
310
+ var spansResponse api_v3.SpansResponseChunk
311
+ parseChunkResponse (t , body , & spansResponse )
312
+
177
313
assert .Len (t , spansResponse .GetResourceSpans (), 1 )
178
314
assert .Equal (t , bytesOfTraceID (t , traceID .High , traceID .Low ), spansResponse .GetResourceSpans ()[0 ].GetScopeSpans ()[0 ].GetSpans ()[0 ].GetTraceId ())
179
315
}
@@ -207,11 +343,6 @@ func TestGRPCGatewayWithBasePathAndTLS(t *testing.T) {
207
343
testGRPCGateway (t , "/jaeger" , serverTLS , clientTLS )
208
344
}
209
345
210
- // For more details why this is needed see https://github.com/grpc-ecosystem/grpc-gateway/issues/2189
211
- type envelope struct {
212
- Result json.RawMessage `json:"result"`
213
- }
214
-
215
346
func TestGRPCGatewayWithTenancy (t * testing.T ) {
216
347
tenancyOptions := tenancy.Options {
217
348
Enabled : true ,
@@ -226,7 +357,7 @@ func TestGRPCGatewayWithTenancy(t *testing.T) {
226
357
})
227
358
}
228
359
229
- func TestTenancyGRPCRejection (t * testing.T ) {
360
+ func TestGRPCGatewayTenancyRejection (t * testing.T ) {
230
361
basePath := "/"
231
362
tenancyOptions := tenancy.Options {Enabled : true }
232
363
gw := setupGRPCGateway (t ,
0 commit comments