Skip to content

Commit bdd43f7

Browse files
authored
Add grpc-gateway tests for all APIv3 methods (#5051)
## Which problem is this PR solving? - Part of #5052 - Continuation of #5046 ## Description of the changes - Add tests for all HTTP APIs, not just GetTrace - Use snapshots to make validation of HTTP/JSON response from the server easier - Replace grpc-gateway/runtime JSONPb marshaler (in tests) with gogo/jsonpb marshaler ## Gaps - The error conditions are not being tested currently, such as not specifying the timestamps in the query ## How was this change tested? - Unit tests --------- Signed-off-by: Yuri Shkuro <[email protected]>
1 parent 54f45e6 commit bdd43f7

14 files changed

+353
-40
lines changed

cmd/query/app/apiv3/grpc_gateway_test.go

Lines changed: 166 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,25 @@
1515
package apiv3
1616

1717
import (
18+
"bytes"
1819
"context"
1920
"encoding/json"
2021
"fmt"
2122
"io"
2223
"net"
2324
"net/http"
25+
"net/url"
26+
"os"
27+
"path/filepath"
2428
"strings"
2529
"testing"
30+
"time"
2631

32+
gogojsonpb "github.com/gogo/protobuf/jsonpb"
33+
gogoproto "github.com/gogo/protobuf/proto"
2734
"github.com/gorilla/mux"
28-
"github.com/grpc-ecosystem/grpc-gateway/runtime"
2935
"github.com/stretchr/testify/assert"
36+
"github.com/stretchr/testify/mock"
3037
"github.com/stretchr/testify/require"
3138
"go.uber.org/zap"
3239
"google.golang.org/grpc"
@@ -39,23 +46,36 @@ import (
3946
"github.com/jaegertracing/jaeger/pkg/tenancy"
4047
"github.com/jaegertracing/jaeger/proto-gen/api_v3"
4148
dependencyStoreMocks "github.com/jaegertracing/jaeger/storage/dependencystore/mocks"
49+
"github.com/jaegertracing/jaeger/storage/spanstore"
4250
spanstoremocks "github.com/jaegertracing/jaeger/storage/spanstore/mocks"
4351
)
4452

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"
4662

4763
type testGateway struct {
4864
reader *spanstoremocks.Reader
4965
url string
5066
}
5167

68+
type gatewayRequest struct {
69+
url string
70+
setupRequest func(*http.Request)
71+
}
72+
5273
func setupGRPCGateway(
5374
t *testing.T,
5475
basePath string,
5576
serverTLS, clientTLS *tlscfg.Options,
5677
tenancyOptions tenancy.Options,
5778
) *testGateway {
58-
// *spanstoremocks.Reader, net.Listener, *grpc.Server, context.CancelFunc, *http.Server
5979
gw := &testGateway{
6080
reader: &spanstoremocks.Reader{},
6181
}
@@ -123,6 +143,64 @@ func setupGRPCGateway(
123143
return gw
124144
}
125145

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+
126204
func testGRPCGateway(
127205
t *testing.T, basePath string,
128206
serverTLS, clientTLS *tlscfg.Options,
@@ -144,36 +222,94 @@ func testGRPCGatewayWithTenancy(
144222
setupRequest func(*http.Request),
145223
) {
146224
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+
}
147238

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()
159241

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)
169284

170-
jsonpb := &runtime.JSONPb{}
171-
var envelope envelope
172-
err = json.Unmarshal(body, &envelope)
173-
require.NoError(t, err)
174285
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+
177313
assert.Len(t, spansResponse.GetResourceSpans(), 1)
178314
assert.Equal(t, bytesOfTraceID(t, traceID.High, traceID.Low), spansResponse.GetResourceSpans()[0].GetScopeSpans()[0].GetSpans()[0].GetTraceId())
179315
}
@@ -207,11 +343,6 @@ func TestGRPCGatewayWithBasePathAndTLS(t *testing.T) {
207343
testGRPCGateway(t, "/jaeger", serverTLS, clientTLS)
208344
}
209345

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-
215346
func TestGRPCGatewayWithTenancy(t *testing.T) {
216347
tenancyOptions := tenancy.Options{
217348
Enabled: true,
@@ -226,7 +357,7 @@ func TestGRPCGatewayWithTenancy(t *testing.T) {
226357
})
227358
}
228359

229-
func TestTenancyGRPCRejection(t *testing.T) {
360+
func TestGRPCGatewayTenancyRejection(t *testing.T) {
230361
basePath := "/"
231362
tenancyOptions := tenancy.Options{Enabled: true}
232363
gw := setupGRPCGateway(t,
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"result": {
3+
"resourceSpans": [
4+
{
5+
"resource": {},
6+
"scopeSpans": [
7+
{
8+
"scope": {},
9+
"spans": [
10+
{
11+
"endTimeUnixNano": "11651379494838206464",
12+
"name": "foobar",
13+
"spanId": "AAAAAAAAALQ=",
14+
"startTimeUnixNano": "11651379494838206464",
15+
"status": {},
16+
"traceId": "AAAAAAAAAJYAAAAAAAAAoA=="
17+
}
18+
]
19+
}
20+
]
21+
}
22+
]
23+
}
24+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"operations": [
3+
{
4+
"name": "get_users",
5+
"spanKind": "server"
6+
}
7+
]
8+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"services": [
3+
"foo"
4+
]
5+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"result": {
3+
"resourceSpans": [
4+
{
5+
"resource": {},
6+
"scopeSpans": [
7+
{
8+
"scope": {},
9+
"spans": [
10+
{
11+
"endTimeUnixNano": "11651379494838206464",
12+
"name": "foobar",
13+
"spanId": "AAAAAAAAALQ=",
14+
"startTimeUnixNano": "11651379494838206464",
15+
"status": {},
16+
"traceId": "AAAAAAAAAJYAAAAAAAAAoA=="
17+
}
18+
]
19+
}
20+
]
21+
}
22+
]
23+
}
24+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"result": {
3+
"resourceSpans": [
4+
{
5+
"resource": {},
6+
"scopeSpans": [
7+
{
8+
"scope": {},
9+
"spans": [
10+
{
11+
"endTimeUnixNano": "11651379494838206464",
12+
"name": "foobar",
13+
"spanId": "AAAAAAAAALQ=",
14+
"startTimeUnixNano": "11651379494838206464",
15+
"status": {},
16+
"traceId": "AAAAAAAAAJYAAAAAAAAAoA=="
17+
}
18+
]
19+
}
20+
]
21+
}
22+
]
23+
}
24+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"operations": [
3+
{
4+
"name": "get_users",
5+
"spanKind": "server"
6+
}
7+
]
8+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"services": [
3+
"foo"
4+
]
5+
}

0 commit comments

Comments
 (0)