From f1a194076ca71e55ee4e9383e3bfe65437aabe08 Mon Sep 17 00:00:00 2001 From: Anton Dudko <52532588+addudko@users.noreply.github.com> Date: Tue, 3 Nov 2020 19:23:03 +0300 Subject: [PATCH] Add unit tests (#203) * B-30111 - Add UTs * B-30111 - Add UTs * Add unit tests --- tracing/annotator.go | 2 +- tracing/annotator_test.go | 28 ++++++ tracing/grpc.go | 6 +- tracing/grpc_test.go | 180 +++++++++++++++++++++++++++++++++ tracing/http.go | 8 +- tracing/http_test.go | 204 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 420 insertions(+), 8 deletions(-) create mode 100644 tracing/annotator_test.go create mode 100644 tracing/grpc_test.go diff --git a/tracing/annotator.go b/tracing/annotator.go index b30c3d1b..f86dc18e 100644 --- a/tracing/annotator.go +++ b/tracing/annotator.go @@ -17,7 +17,7 @@ const ( var defaultFormat propagation.HTTPFormat = &b3.HTTPFormat{} //SpanContextAnnotator retrieve information about current span from context or HTTP headers -//and propogate in binary format to gRPC service +//and propagate in binary format to gRPC service func SpanContextAnnotator(ctx context.Context, req *http.Request) metadata.MD { md := make(metadata.MD) diff --git a/tracing/annotator_test.go b/tracing/annotator_test.go new file mode 100644 index 00000000..9e9c4f94 --- /dev/null +++ b/tracing/annotator_test.go @@ -0,0 +1,28 @@ +package tracing + +import ( + "context" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "go.opencensus.io/trace" + "go.opencensus.io/trace/propagation" +) + +func TestSpanContextAnnotator_FromContext(t *testing.T) { + ctx, span := trace.StartSpan(context.Background(), "") + result := SpanContextAnnotator(ctx, nil) + assert.Equal(t, []string{string(propagation.Binary(span.SpanContext()))}, result[traceContextKey]) +} + +func TestSpanContextAnnotator_FromRequest(t *testing.T) { + _, span := trace.StartSpan(context.Background(), "") + req, _ := http.NewRequest("", "", nil) + defaultFormat.SpanContextToRequest(span.SpanContext(), req) + sc, ok := defaultFormat.SpanContextFromRequest(req) + assert.True(t, ok) + + result := SpanContextAnnotator(context.Background(), req) + assert.Equal(t, []string{string(propagation.Binary(sc))}, result[traceContextKey]) +} diff --git a/tracing/grpc.go b/tracing/grpc.go index 869d3aab..5db4e8a2 100644 --- a/tracing/grpc.go +++ b/tracing/grpc.go @@ -123,7 +123,7 @@ func (s *ServerHandler) HandleRPC(ctx context.Context, rs stats.RPCStats) { attrs := metadataToAttributes(rs.Trailer, RequestTrailerAnnotationPrefix, s.options.metadataMatcher) span.AddAttributes(attrs...) case *stats.OutHeader: - attrs := metadataToAttributes(rs.Header, ResponsePayloadAnnotationKey, s.options.metadataMatcher) + attrs := metadataToAttributes(rs.Header, ResponseHeaderAnnotationPrefix, s.options.metadataMatcher) span.AddAttributes(attrs...) case *stats.OutTrailer: attrs := metadataToAttributes(rs.Trailer, ResponseTrailerAnnotationPrefix, s.options.metadataMatcher) @@ -162,10 +162,10 @@ func (s *ServerHandler) HandleRPC(ctx context.Context, rs stats.RPCStats) { } -func metadataToAttributes(md metadata.MD, prefix string, marcher metadataMatcher) []trace.Attribute { +func metadataToAttributes(md metadata.MD, prefix string, matcher metadataMatcher) []trace.Attribute { attrs := make([]trace.Attribute, 0, len(md)) for k, vals := range md { - key, ok := marcher(k) + key, ok := matcher(k) if !ok { continue } diff --git a/tracing/grpc_test.go b/tracing/grpc_test.go new file mode 100644 index 00000000..1a3c11be --- /dev/null +++ b/tracing/grpc_test.go @@ -0,0 +1,180 @@ +package tracing + +import ( + "context" + "fmt" + "reflect" + "testing" + + "github.com/stretchr/testify/assert" + "go.opencensus.io/trace" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/stats" +) + +var testGRPCOptions = &gRPCOptions{} + +func TestDefaultGRPCOptions(t *testing.T) { + expected := &gRPCOptions{ + metadataMatcher: defaultMetadataMatcher, + maxPayloadSize: 1048576, + } + + result := defaultGRPCOptions() + expectedHeader, expectedBool := expected.metadataMatcher(expectedStr) + resultHeader, resultBool := result.metadataMatcher(expectedStr) + assert.True(t, expectedBool) + assert.Equal(t, expectedBool, resultBool) + assert.Equal(t, expectedHeader, resultHeader) + assert.Equal(t, expected.maxPayloadSize, result.maxPayloadSize) +} + +func TestWithMetadataAnnotation(t *testing.T) { + option := WithMetadataAnnotation(func(ctx context.Context, stats stats.RPCStats) bool { + return true + }) + option(testGRPCOptions) + assert.True(t, testGRPCOptions.spanWithMetadata(nil, nil)) +} + +func TestWithMetadataMatcher(t *testing.T) { + option := WithMetadataMatcher(func(s string) (string, bool) { + return s, true + }) + option(testGRPCOptions) + resultStr, ok := testGRPCOptions.metadataMatcher(expectedStr) + assert.True(t, ok) + assert.Equal(t, expectedStr, resultStr) +} + +func TestWithGRPCPayloadAnnotation(t *testing.T) { + option := WithGRPCPayloadAnnotation(func(ctx context.Context, rpcStats stats.RPCStats) bool { + return true + }) + option(testGRPCOptions) + assert.True(t, testGRPCOptions.spanWithPayload(nil, nil)) +} + +func TestWithGRPCPayloadLimit(t *testing.T) { + option := WithGRPCPayloadLimit(333) + option(testGRPCOptions) + assert.Equal(t, 333, testGRPCOptions.maxPayloadSize) +} + +func TestNewServerHandler(t *testing.T) { + result := NewServerHandler(func(options *gRPCOptions) { + options.spanWithPayload = func(ctx context.Context, rpcStats stats.RPCStats) bool { + return true + } + }) + + matcherStr, ok := result.options.metadataMatcher(expectedStr) + assert.True(t, ok) + assert.Equal(t, expectedStr, matcherStr) + assert.True(t, result.options.spanWithPayload(nil, nil)) + assert.Equal(t, DefaultMaxPayloadSize, result.options.maxPayloadSize) +} + +func TestServerHandler_HandleRPC(t *testing.T) { + handler := NewServerHandler(func(options *gRPCOptions) { + options.spanWithPayload = func(ctx context.Context, rpcStats stats.RPCStats) bool { + return true + } + + options.spanWithMetadata = func(ctx context.Context, rpcStats stats.RPCStats) bool { + return true + } + }) + + expectedStats := []stats.RPCStats{ + &stats.End{ + Error: fmt.Errorf(""), + }, + &stats.InHeader{ + Header: map[string][]string{ + "header1": {""}, + }, + }, + &stats.InTrailer{ + Trailer: map[string][]string{ + "trailer1": {""}, + }, + }, + &stats.OutHeader{ + Header: map[string][]string{ + "outHeader1": {""}, + }, + }, + &stats.OutTrailer{ + Trailer: map[string][]string{ + "outTrailer1": {""}, + }, + }, + &stats.InPayload{ + Payload: []byte(""), + }, + &stats.OutPayload{ + Payload: []byte(""), + }, + } + + ctx, _ := trace.StartSpan(context.Background(), "test span", trace.WithSampler(trace.AlwaysSample())) + + for _, v := range expectedStats { + handler.HandleRPC(ctx, v) + } + + expectedMap := map[string]string{ + "request.header.header1": "true", + "request.trailer.trailer1": "true", + "response.header.outHeader1": "true", + "response.trailer.outTrailer1": "true", + } + + resultMap := make(map[string]string, 4) + reflectAttrs := reflect.ValueOf(trace.FromContext(ctx)).Elem().Field(3).Elem().Field(0) + reflectKeys := reflectAttrs.MapKeys() + for _, k := range reflectKeys { + key := k.Convert(reflectAttrs.Type().Key()) + val := reflectAttrs.MapIndex(key) + resultMap[fmt.Sprint(key)] = fmt.Sprint(val) + } + + assert.Equal(t, expectedMap, resultMap) + + expectedAnnotations := []string{ + "Response error", "Request payload", "Response payload", + } + + resultAnnotations := make([]string, 0, 3) + reflectedAnnotations := reflect.ValueOf(trace.FromContext(ctx)).Elem().Field(4).Elem().Field(0).Slice(0, 3) + for i := 0; i < 3; i++ { + resultAnnotations = append(resultAnnotations, fmt.Sprint(reflectedAnnotations.Index(i).Elem().Field(1))) + } + + assert.Equal(t, expectedAnnotations, resultAnnotations) +} + +func TestMetadataToAttributes(t *testing.T) { + expected := []trace.Attribute{trace.StringAttribute(fmt.Sprint("prefix.", expectedStr), "test value")} + result := metadataToAttributes(metadata.MD{expectedStr: {"test value"}}, "prefix.", defaultMetadataMatcher) + assert.Equal(t, expected, result) +} + +func TestPayloadToAttributes(t *testing.T) { + expected := trace.StringAttribute(expectedStr, "\"test value\"") + result, ok, err := payloadToAttributes(expectedStr, "test value", 12) + assert.NoError(t, err) + assert.False(t, ok) + assert.Equal(t, expected, result[0]) +} + +func TestDefaultMetadataMatcher(t *testing.T) { + resultStr, ok := defaultMetadataMatcher(expectedStr) + assert.True(t, ok) + assert.Equal(t, expectedStr, resultStr) +} + +func TestAlwaysGRPC(t *testing.T) { + assert.True(t, AlwaysGRPC(nil, nil)) +} diff --git a/tracing/http.go b/tracing/http.go index 083dd3e3..2e32008b 100644 --- a/tracing/http.go +++ b/tracing/http.go @@ -22,7 +22,7 @@ const ( ResponseHeaderAnnotationPrefix = "response.header." //ResponseTrailerAnnotationPrefix is a prefix which is added to each response header attribute - ResponseTrailerAnnotationPrefix = "request.trailer." + ResponseTrailerAnnotationPrefix = "response.trailer." //RequestPayloadAnnotationKey is a key under which request payload stored in span RequestPayloadAnnotationKey = "request.payload" @@ -94,7 +94,7 @@ func WithPayloadAnnotation(f func(*http.Request) bool) HTTPOption { } } -//WithHTTPPayloadSize limit payload size propogated to span +//WithHTTPPayloadSize limit payload size propagated to span //in case payload exceeds limit, payload truncated and //annotation payload.truncated=true added into span func WithHTTPPayloadSize(maxSize int) HTTPOption { @@ -125,7 +125,7 @@ func NewMiddleware(ops ...HTTPOption) func(http.Handler) http.Handler { //Check that &Handler comply with http.Handler interface var _ http.Handler = &Handler{} -//Handler is a opencensus http plugin wrapper which do some usefull things to reach traces +//Handler is a opencensus http plugin wrapper which do some useful things to reach traces type Handler struct { child http.Handler @@ -219,7 +219,7 @@ type responseBodyWrapper struct { } func (w *responseBodyWrapper) Write(b []byte) (int, error) { - //In case we recieve an error from Writing into buffer we just skip it + //In case we receive an error from Writing into buffer we just skip it //because adding payload to span is not so critical as provide response _, _ = w.buffer.Write(b) return w.ResponseWriter.Write(b) diff --git a/tracing/http_test.go b/tracing/http_test.go index dab116b1..1733d41d 100644 --- a/tracing/http_test.go +++ b/tracing/http_test.go @@ -1,9 +1,22 @@ package tracing import ( + "bytes" + "context" + "fmt" + "net/http" + "net/http/httptest" + "reflect" "testing" + + "github.com/stretchr/testify/assert" + "go.opencensus.io/plugin/ochttp" + "go.opencensus.io/trace" ) +var testHTTPOpts = &httpOptions{} +var expectedStr = "test" + func Test_truncatePayload(t *testing.T) { tests := []struct { in []byte @@ -70,3 +83,194 @@ func Test_obfuscate(t *testing.T) { } } } + +func TestDefaultHTTPOptions(t *testing.T) { + expected := &httpOptions{ + headerMatcher: defaultHeaderMatcher, + maxPayloadSize: 1048576, + } + + result := defaultHTTPOptions() + expectedHeader, expectedBool := expected.headerMatcher(expectedStr) + resultHeader, resultBool := result.headerMatcher(expectedStr) + assert.True(t, expectedBool) + assert.Equal(t, expectedBool, resultBool) + assert.Equal(t, expectedHeader, resultHeader) + assert.Equal(t, expected.maxPayloadSize, result.maxPayloadSize) +} + +func TestWithHeadersAnnotation(t *testing.T) { + option := WithHeadersAnnotation(func(r *http.Request) bool { + return true + }) + option(testHTTPOpts) + assert.True(t, testHTTPOpts.spanWithHeaders(nil)) +} + +func TestWithHeaderMatcher(t *testing.T) { + option := WithHeaderMatcher(defaultHeaderMatcher) + option(testHTTPOpts) + resultStr, ok := testHTTPOpts.headerMatcher(expectedStr) + assert.True(t, ok) + assert.Equal(t, expectedStr, resultStr) +} + +func TestWithPayloadAnnotation(t *testing.T) { + option := WithPayloadAnnotation(func(r *http.Request) bool { + return true + }) + option(testHTTPOpts) + assert.True(t, testHTTPOpts.spanWithPayload(nil)) +} + +func TestWithHTTPPayloadSize(t *testing.T) { + option := WithHTTPPayloadSize(333) + option(testHTTPOpts) + assert.Equal(t, 333, testHTTPOpts.maxPayloadSize) +} + +func TestNewMiddleware(t *testing.T) { + handlerFunc := NewMiddleware(func(options *httpOptions) { + options.spanWithHeaders = func(r *http.Request) bool { + return true + } + }) + + handler := handlerFunc(&httpHandlerMock{}) + ocHandler, ok := handler.(*ochttp.Handler) + assert.True(t, ok) + + result, ok := (ocHandler.Handler).(*Handler) + assert.True(t, ok) + assert.True(t, result.options.spanWithHeaders(nil)) +} + +func TestHandler_ServeHTTP(t *testing.T) { + handlerFunc := NewMiddleware(func(options *httpOptions) { + options.spanWithHeaders = func(r *http.Request) bool { + return true + } + + options.spanWithPayload = func(r *http.Request) bool { + return true + } + }) + + ctx, _ := trace.StartSpan(context.Background(), "test span", trace.WithSampler(trace.AlwaysSample())) + + r, _ := http.NewRequest("", "", bytes.NewBuffer([]byte("test body"))) + r.Header = map[string][]string{ + "test1": {"test11"}, + "test2": {"test22"}, + } + r = r.WithContext(ctx) + + w := httptest.NewRecorder() + w.Header().Add("test3", "") + + result := &httpHandlerMock{} + handler := handlerFunc(result) + handler.ServeHTTP(w, r) + resp := w.Result() + assert.Equal(t, http.StatusOK, resp.StatusCode) + + // Test that Span attributes were populated with Headers + reflectAttrMap := reflect.ValueOf(trace.FromContext(result.request.Context())).Elem().Field(3).Elem().Field(0) + reflectKeys := reflectAttrMap.MapKeys() + assert.Len(t, reflectKeys, 8) + + resultHeadersMap := make(map[string]string) + for _, k := range reflectKeys { + key := k.Convert(reflectAttrMap.Type().Key()) + val := reflectAttrMap.MapIndex(key) + resultHeadersMap[fmt.Sprint(key)] = fmt.Sprint(val) + } + assert.Equal(t, "true", resultHeadersMap[fmt.Sprint(RequestHeaderAnnotationPrefix, "test1")]) + assert.Equal(t, "true", resultHeadersMap[fmt.Sprint(RequestHeaderAnnotationPrefix, "test2")]) + assert.Equal(t, "true", resultHeadersMap[fmt.Sprint(ResponseHeaderAnnotationPrefix, "Test3")]) + + // Test that Span annotations were populated with payload attributes and annotation messages + reflectAnnotations := reflect.ValueOf(trace.FromContext(result.request.Context())).Elem().Field(4).Elem().Field(0).Slice(0, 2) + resultRequestPayloadMsg := fmt.Sprint(reflectAnnotations.Index(0).Elem().Field(1)) + resultResponsePayloadMsg := fmt.Sprint(reflectAnnotations.Index(1).Elem().Field(1)) + assert.Equal(t, "Request payload", resultRequestPayloadMsg) + assert.Equal(t, "Response payload", resultResponsePayloadMsg) + + // Test that Span contains given payload + reflectAttrMap = reflectAnnotations.Index(0).Elem().Field(2) + reflectKeys = reflectAttrMap.MapKeys() + resultPayload := fmt.Sprint(reflectAttrMap.MapIndex(reflectKeys[0])) + assert.Equal(t, "test body", resultPayload) +} + +func TestHeadersToAttributes(t *testing.T) { + expected := append(make([]trace.Attribute, 0, 2), trace.StringAttribute("prefix-test1", "test11"), trace.StringAttribute("prefix-test2", "test22")) + testHeaders := map[string][]string{ + "test1": {"test11"}, + "test2": {"test22"}, + } + + result := headersToAttributes(testHeaders, "prefix-", defaultHeaderMatcher) + assert.Len(t, result, 2) + assert.Equal(t, expected, result) +} + +func TestMarkSpanTruncated(t *testing.T) { + _, span := trace.StartSpan(context.Background(), "test span", trace.WithSampler(trace.AlwaysSample())) + markSpanTruncated(span) + + reflectAttrMap := reflect.ValueOf(span).Elem().Field(3).Elem().Field(0) + reflectKeys := reflectAttrMap.MapKeys() + assert.Len(t, reflectKeys, 1) + + reflectKey := reflectKeys[0].Convert(reflectAttrMap.Type().Key()) + resultKey := fmt.Sprint(reflectKey) + resultValue := fmt.Sprint(reflectAttrMap.MapIndex(reflectKey)) + assert.Equal(t, TruncatedMarkerKey, resultKey) + assert.Equal(t, resultValue, TruncatedMarkerValue) +} + +func TestNewResponseWrapper(t *testing.T) { + expected := &bytes.Buffer{} + result := newResponseWrapper(&httpResponseWriterMock{}) + assert.NotNil(t, result.ResponseWriter) + assert.Equal(t, expected, result.buffer) +} + +func TestResponseBodyWrapper_Write(t *testing.T) { + wrapper := newResponseWrapper(&httpResponseWriterMock{}) + result, err := wrapper.Write([]byte("3")) + assert.NoError(t, err) + assert.Equal(t, 0, result) +} + +func TestDefaultHeaderMatcher(t *testing.T) { + result, ok := defaultHeaderMatcher(expectedStr) + assert.Equal(t, expectedStr, result) + assert.True(t, ok) +} + +func TestAlwaysHTTP(t *testing.T) { + test, _ := http.NewRequest("", "", nil) + result := AlwaysHTTP(test) + assert.True(t, result) +} + +type httpResponseWriterMock struct { + http.ResponseWriter +} + +func (fake *httpResponseWriterMock) Write(_ []byte) (int, error) { + return 0, nil +} + +type httpHandlerMock struct { + http.Handler + writer http.ResponseWriter + request *http.Request +} + +func (fake *httpHandlerMock) ServeHTTP(w http.ResponseWriter, r *http.Request) { + fake.writer = w + fake.request = r +}