Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add grpc-gateway tests for all APIv3 methods #5051

Merged
merged 6 commits into from
Dec 28, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
205 changes: 171 additions & 34 deletions cmd/query/app/apiv3/grpc_gateway_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,17 @@ import (
"io"
"net"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"testing"
"time"

"github.com/gorilla/mux"
"github.com/grpc-ecosystem/grpc-gateway/runtime"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
"google.golang.org/grpc"
Expand All @@ -39,23 +44,36 @@ import (
"github.com/jaegertracing/jaeger/pkg/tenancy"
"github.com/jaegertracing/jaeger/proto-gen/api_v3"
dependencyStoreMocks "github.com/jaegertracing/jaeger/storage/dependencystore/mocks"
"github.com/jaegertracing/jaeger/storage/spanstore"
spanstoremocks "github.com/jaegertracing/jaeger/storage/spanstore/mocks"
)

var testCertKeyLocation = "../../../../pkg/config/tlscfg/testdata/"
const (
testCertKeyLocation = "../../../../pkg/config/tlscfg/testdata/"
snapshotLocation = "./snapshots/"
)

// Snapshots can be regenerated via:
//
// REGENERATE_SNAPSHOTS=true go test -v ./cmd/query/app/apiv3/...
var regenerateSnapshots = os.Getenv("REGENERATE_SNAPSHOTS") == "true"

type testGateway struct {
reader *spanstoremocks.Reader
url string
}

type gatewayRequest struct {
url string
setupRequest func(*http.Request)
}

func setupGRPCGateway(
t *testing.T,
basePath string,
serverTLS, clientTLS *tlscfg.Options,
tenancyOptions tenancy.Options,
) *testGateway {
// *spanstoremocks.Reader, net.Listener, *grpc.Server, context.CancelFunc, *http.Server
gw := &testGateway{
reader: &spanstoremocks.Reader{},
}
Expand Down Expand Up @@ -123,6 +141,66 @@ func setupGRPCGateway(
return gw
}

func (gw *testGateway) execRequest(t *testing.T, gwReq *gatewayRequest) ([]byte, int) {
req, err := http.NewRequest(http.MethodGet, gw.url+gwReq.url, nil)
require.NoError(t, err)
req.Header.Set("Content-Type", "application/json")
gwReq.setupRequest(req)
response, err := http.DefaultClient.Do(req)
require.NoError(t, err)
body, err := io.ReadAll(response.Body)
require.NoError(t, err)
require.NoError(t, response.Body.Close())
return body, response.StatusCode
}

func verifySnapshot(t *testing.T, body []byte) []byte {
// reformat JSON body with indentation, to make diffing easier
var data interface{}
require.NoError(t, json.Unmarshal(body, &data))
body, err := json.MarshalIndent(data, "", " ")
require.NoError(t, err)

snapshotFile := filepath.Join(snapshotLocation, strings.ReplaceAll(t.Name(), "/", "_")+".json")
if regenerateSnapshots {
os.WriteFile(snapshotFile, body, 0o644)
}
snapshot, err := os.ReadFile(snapshotFile)
require.NoError(t, err)
assert.Equal(t, string(snapshot), string(body), "comparing against stored snapshot. Use REGENERATE_SNAPSHOTS=true to rebuild snapshots.")
return body
}

func parseResponse(t *testing.T, body []byte, obj interface{}) {
// TODO can we use gogo marshaler here instead of grcp-gateway?
jsonpb := &runtime.JSONPb{}
require.NoError(t, jsonpb.Unmarshal(body, obj))
}

func parseChunkResponse(t *testing.T, body []byte, obj interface{}) {
// Unwrap the 'result' container generated by the gateway.
// See https://github.com/grpc-ecosystem/grpc-gateway/issues/2189
type resultWrapper struct {
Result json.RawMessage `json:"result"`
}
var result resultWrapper
require.NoError(t, json.Unmarshal(body, &result))
parseResponse(t, result.Result, obj)
}

func makeTestTrace() (*model.Trace, model.TraceID) {
traceID := model.NewTraceID(150, 160)
return &model.Trace{
Spans: []*model.Span{
{
TraceID: traceID,
SpanID: model.NewSpanID(180),
OperationName: "foobar",
},
},
}, traceID
}

func testGRPCGateway(
t *testing.T, basePath string,
serverTLS, clientTLS *tlscfg.Options,
Expand All @@ -144,36 +222,100 @@ func testGRPCGatewayWithTenancy(
setupRequest func(*http.Request),
) {
gw := setupGRPCGateway(t, basePath, serverTLS, clientTLS, tenancyOptions)
t.Run("GetServices", func(t *testing.T) {
runGatewayGetServices(t, gw, setupRequest)
})
t.Run("GetOperations", func(t *testing.T) {
runGatewayGetOperations(t, gw, setupRequest)
})
t.Run("GetTrace", func(t *testing.T) {
runGatewayGetTrace(t, gw, setupRequest)
})
t.Run("FindTraces", func(t *testing.T) {
runGatewayFindTraces(t, gw, setupRequest)
})
}
Copy link
Member Author

@yurishkuro yurishkuro Dec 27, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@albertteoh this was the point of cleaning up tests - now they are testing all gateway APIs and we can try things like upgrading the gateway dep to v2.x or completely re-implementing it. My next step would be to try an upgrade, which requires building a new protoc image that I just kicked off (takes 4hrs to build), and maybe with v2 gateway we could find a way to integrate gogo marshaling.


traceID := model.NewTraceID(150, 160)
gw.reader.On("GetTrace", matchContext, matchTraceID).Return(
&model.Trace{
Spans: []*model.Span{
{
TraceID: traceID,
SpanID: model.NewSpanID(180),
OperationName: "foobar",
},
},
}, nil).Once()
func runGatewayGetServices(t *testing.T, gw *testGateway, setupRequest func(*http.Request)) {
gw.reader.On("GetServices", matchContext).Return([]string{"foo"}, nil).Once()

req, err := http.NewRequest(http.MethodGet, gw.url+"/api/v3/traces/123", nil)
require.NoError(t, err)
req.Header.Set("Content-Type", "application/json")
setupRequest(req)
response, err := http.DefaultClient.Do(req)
require.NoError(t, err)
body, err := io.ReadAll(response.Body)
require.NoError(t, err)
require.NoError(t, response.Body.Close())
body, statusCode := gw.execRequest(t, &gatewayRequest{
url: "/api/v3/services",
setupRequest: setupRequest,
})
require.Equal(t, http.StatusOK, statusCode)
body = verifySnapshot(t, body)

var response struct {
Services []string `json:"services"`
}
parseResponse(t, body, &response)
assert.Equal(t, []string{"foo"}, response.Services)
}

func runGatewayGetOperations(t *testing.T, gw *testGateway, setupRequest func(*http.Request)) {
gw.reader.
On("GetOperations", matchContext, mock.AnythingOfType("spanstore.OperationQueryParameters")).
Return([]spanstore.Operation{{Name: "get_users", SpanKind: "server"}}, nil).Once()

body, statusCode := gw.execRequest(t, &gatewayRequest{
url: "/api/v3/operations",
setupRequest: setupRequest,
})
require.Equal(t, http.StatusOK, statusCode)
body = verifySnapshot(t, body)

var response struct {
Operations []struct {
Name string `json:"name"`
SpanKind string `json:"spanKind"`
} `json:"operations"`
}
parseResponse(t, body, &response)
require.Len(t, response.Operations, 1)
assert.Equal(t, "get_users", response.Operations[0].Name)
assert.Equal(t, "server", response.Operations[0].SpanKind)
}

func runGatewayGetTrace(t *testing.T, gw *testGateway, setupRequest func(*http.Request)) {
trace, traceID := makeTestTrace()
gw.reader.On("GetTrace", matchContext, matchTraceID).Return(trace, nil).Once()

body, statusCode := gw.execRequest(t, &gatewayRequest{
url: "/api/v3/traces/123",
setupRequest: setupRequest,
})
require.Equal(t, http.StatusOK, statusCode)
body = verifySnapshot(t, body)

jsonpb := &runtime.JSONPb{}
var envelope envelope
err = json.Unmarshal(body, &envelope)
require.NoError(t, err)
var spansResponse api_v3.SpansResponseChunk
err = jsonpb.Unmarshal(envelope.Result, &spansResponse)
require.NoError(t, err)
parseChunkResponse(t, body, &spansResponse)

assert.Len(t, spansResponse.GetResourceSpans(), 1)
assert.Equal(t, bytesOfTraceID(t, traceID.High, traceID.Low), spansResponse.GetResourceSpans()[0].GetScopeSpans()[0].GetSpans()[0].GetTraceId())
}

func runGatewayFindTraces(t *testing.T, gw *testGateway, setupRequest func(*http.Request)) {
trace, traceID := makeTestTrace()
gw.reader.
On("FindTraces", matchContext, mock.AnythingOfType("*spanstore.TraceQueryParameters")).
Return([]*model.Trace{trace}, nil).Once()

q := url.Values{}
q.Set("query.service_name", "foobar")
q.Set("query.start_time_min", time.Now().Format(time.RFC3339))
q.Set("query.start_time_max", time.Now().Format(time.RFC3339))

body, statusCode := gw.execRequest(t, &gatewayRequest{
url: "/api/v3/traces?" + q.Encode(),
setupRequest: setupRequest,
})
require.Equal(t, http.StatusOK, statusCode, "response=%s", string(body))
body = verifySnapshot(t, body)

var spansResponse api_v3.SpansResponseChunk
parseChunkResponse(t, body, &spansResponse)

assert.Len(t, spansResponse.GetResourceSpans(), 1)
assert.Equal(t, bytesOfTraceID(t, traceID.High, traceID.Low), spansResponse.GetResourceSpans()[0].GetScopeSpans()[0].GetSpans()[0].GetTraceId())
}
Expand Down Expand Up @@ -207,11 +349,6 @@ func TestGRPCGatewayWithBasePathAndTLS(t *testing.T) {
testGRPCGateway(t, "/jaeger", serverTLS, clientTLS)
}

// For more details why this is needed see https://github.com/grpc-ecosystem/grpc-gateway/issues/2189
type envelope struct {
Result json.RawMessage `json:"result"`
}

func TestGRPCGatewayWithTenancy(t *testing.T) {
tenancyOptions := tenancy.Options{
Enabled: true,
Expand All @@ -226,7 +363,7 @@ func TestGRPCGatewayWithTenancy(t *testing.T) {
})
}

func TestTenancyGRPCRejection(t *testing.T) {
func TestGRPCGatewayTenancyRejection(t *testing.T) {
basePath := "/"
tenancyOptions := tenancy.Options{Enabled: true}
gw := setupGRPCGateway(t,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"result": {
"resourceSpans": [
{
"resource": {},
"scopeSpans": [
{
"scope": {},
"spans": [
{
"endTimeUnixNano": "11651379494838206464",
"name": "foobar",
"spanId": "AAAAAAAAALQ=",
"startTimeUnixNano": "11651379494838206464",
"status": {},
"traceId": "AAAAAAAAAJYAAAAAAAAAoA=="
}
]
}
]
}
]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"operations": [
{
"name": "get_users",
"spanKind": "server"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"services": [
"foo"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"result": {
"resourceSpans": [
{
"resource": {},
"scopeSpans": [
{
"scope": {},
"spans": [
{
"endTimeUnixNano": "11651379494838206464",
"name": "foobar",
"spanId": "AAAAAAAAALQ=",
"startTimeUnixNano": "11651379494838206464",
"status": {},
"traceId": "AAAAAAAAAJYAAAAAAAAAoA=="
}
]
}
]
}
]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"result": {
"resourceSpans": [
{
"resource": {},
"scopeSpans": [
{
"scope": {},
"spans": [
{
"endTimeUnixNano": "11651379494838206464",
"name": "foobar",
"spanId": "AAAAAAAAALQ=",
"startTimeUnixNano": "11651379494838206464",
"status": {},
"traceId": "AAAAAAAAAJYAAAAAAAAAoA=="
}
]
}
]
}
]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"operations": [
{
"name": "get_users",
"spanKind": "server"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"services": [
"foo"
]
}
Loading