github.com/newrelic/go-agent@v3.26.0+incompatible/internal/synthetics_test.go (about)

     1  // Copyright 2020 New Relic Corporation. All rights reserved.
     2  // SPDX-License-Identifier: Apache-2.0
     3  
     4  package internal
     5  
     6  import (
     7  	"encoding/json"
     8  	"fmt"
     9  	"net/http"
    10  	"testing"
    11  
    12  	"github.com/newrelic/go-agent/internal/cat"
    13  	"github.com/newrelic/go-agent/internal/crossagent"
    14  )
    15  
    16  type harvestedTxnTrace struct {
    17  	startTimeMs        float64
    18  	durationToResponse float64
    19  	transactionName    string
    20  	requestURL         string
    21  	traceDetails       struct {
    22  		attributes struct {
    23  			agentAttributes eventAttributes
    24  			userAttributes  eventAttributes
    25  			intrinsics      eventAttributes
    26  		}
    27  	}
    28  	catGUID              string
    29  	forcePersistFlag     bool
    30  	xraySessionID        string
    31  	syntheticsResourceID string
    32  }
    33  
    34  func (h *harvestedTxnTrace) UnmarshalJSON(data []byte) error {
    35  	var arr []interface{}
    36  
    37  	if err := json.Unmarshal(data, &arr); err != nil {
    38  		return err
    39  	}
    40  
    41  	if len(arr) != 10 {
    42  		return fmt.Errorf("unexpected number of transaction trace items: %d", len(arr))
    43  	}
    44  
    45  	h.startTimeMs = arr[0].(float64)
    46  	h.durationToResponse = arr[1].(float64)
    47  	h.transactionName = arr[2].(string)
    48  	if nil != arr[3] {
    49  		h.requestURL = arr[3].(string)
    50  	}
    51  	// Item 4 -- the trace -- will be dealt with shortly.
    52  	h.catGUID = arr[5].(string)
    53  	// Item 6 intentionally ignored.
    54  	h.forcePersistFlag = arr[7].(bool)
    55  	if arr[8] != nil {
    56  		h.xraySessionID = arr[8].(string)
    57  	}
    58  	h.syntheticsResourceID = arr[9].(string)
    59  
    60  	traceDetails := arr[4].([]interface{})
    61  	attributes := traceDetails[4].(map[string]interface{})
    62  
    63  	h.traceDetails.attributes.agentAttributes = attributes["agentAttributes"].(map[string]interface{})
    64  	h.traceDetails.attributes.userAttributes = attributes["userAttributes"].(map[string]interface{})
    65  	h.traceDetails.attributes.intrinsics = attributes["intrinsics"].(map[string]interface{})
    66  
    67  	return nil
    68  }
    69  
    70  func harvestTxnDataTrace(t *TxnData) (*harvestedTxnTrace, error) {
    71  	// Since transaction trace JSON is built using string manipulation, we have
    72  	// to do an awkward marshal/unmarshal shuffle to be able to verify the
    73  	// intrinsics.
    74  	ht := HarvestTrace{
    75  		TxnEvent: t.TxnEvent,
    76  		Trace:    t.TxnTrace,
    77  	}
    78  	js, err := ht.MarshalJSON()
    79  	if err != nil {
    80  		return nil, err
    81  	}
    82  
    83  	trace := &harvestedTxnTrace{}
    84  	if err := json.Unmarshal(js, trace); err != nil {
    85  		return nil, err
    86  	}
    87  
    88  	return trace, nil
    89  }
    90  
    91  func TestSynthetics(t *testing.T) {
    92  	var testcases []struct {
    93  		Name     string `json:"name"`
    94  		Settings struct {
    95  			AgentEncodingKey      string `json:"agentEncodingKey"`
    96  			SyntheticsEncodingKey string `json:"syntheticsEncodingKey"`
    97  			TransactionGUID       string `json:"transactionGuid"`
    98  			TrustedAccountIDs     []int  `json:"trustedAccountIds"`
    99  		} `json:"settings"`
   100  		InputHeaderPayload     json.RawMessage   `json:"inputHeaderPayload"`
   101  		InputObfuscatedHeader  map[string]string `json:"inputObfuscatedHeader"`
   102  		OutputTransactionTrace struct {
   103  			Header struct {
   104  				Field9 string `json:"field_9"`
   105  			} `json:"header"`
   106  			ExpectedIntrinsics    map[string]string `json:"expectedIntrinsics"`
   107  			NonExpectedIntrinsics []string          `json:"nonExpectedIntrinsics"`
   108  		} `json:"outputTransactionTrace"`
   109  		OutputTransactionEvent struct {
   110  			ExpectedAttributes    map[string]string `json:"expectedAttributes"`
   111  			NonExpectedAttributes []string          `json:"nonExpectedAttributes"`
   112  		} `json:"outputTransactionEvent"`
   113  		OutputExternalRequestHeader struct {
   114  			ExpectedHeader    map[string]string `json:"expectedHeader"`
   115  			NonExpectedHeader []string          `json:"nonExpectedHeader"`
   116  		} `json:"outputExternalRequestHeader"`
   117  	}
   118  
   119  	err := crossagent.ReadJSON("synthetics/synthetics.json", &testcases)
   120  	if err != nil {
   121  		t.Fatal(err)
   122  	}
   123  
   124  	for _, tc := range testcases {
   125  		// Fake enough transaction data to run the test.
   126  		tr := &TxnData{
   127  			Name: "txn",
   128  		}
   129  
   130  		tr.CrossProcess.Init(false, false, &ConnectReply{
   131  			CrossProcessID:  "1#1",
   132  			TrustedAccounts: make(map[int]struct{}),
   133  			EncodingKey:     tc.Settings.AgentEncodingKey,
   134  		})
   135  
   136  		// Set up the trusted accounts.
   137  		for _, account := range tc.Settings.TrustedAccountIDs {
   138  			tr.CrossProcess.TrustedAccounts[account] = struct{}{}
   139  		}
   140  
   141  		// Set up the GUID.
   142  		if tc.Settings.TransactionGUID != "" {
   143  			tr.CrossProcess.GUID = tc.Settings.TransactionGUID
   144  		}
   145  
   146  		// Parse the input header, ignoring any errors.
   147  		inputHeaders := make(http.Header)
   148  		for k, v := range tc.InputObfuscatedHeader {
   149  			inputHeaders.Add(k, v)
   150  		}
   151  
   152  		tr.CrossProcess.handleInboundRequestEncodedSynthetics(inputHeaders.Get(cat.NewRelicSyntheticsName))
   153  
   154  		// Get the headers for an external request.
   155  		metadata, err := tr.CrossProcess.CreateCrossProcessMetadata("txn", "app")
   156  		if err != nil {
   157  			t.Fatalf("%s: error creating outbound request headers: %v", tc.Name, err)
   158  		}
   159  
   160  		// Verify that the header either exists or doesn't exist, depending on the
   161  		// test case.
   162  		headers := MetadataToHTTPHeader(metadata)
   163  		for key, value := range tc.OutputExternalRequestHeader.ExpectedHeader {
   164  			obfuscated := headers.Get(key)
   165  			if obfuscated == "" {
   166  				t.Errorf("%s: expected output header %s not found", tc.Name, key)
   167  			} else if value != obfuscated {
   168  				t.Errorf("%s: expected output header %s mismatch: expected=%s; got=%s", tc.Name, key, value, obfuscated)
   169  			}
   170  		}
   171  
   172  		for _, key := range tc.OutputExternalRequestHeader.NonExpectedHeader {
   173  			if value := headers.Get(key); value != "" {
   174  				t.Errorf("%s: output header %s expected to be missing; got %s", tc.Name, key, value)
   175  			}
   176  		}
   177  
   178  		// Harvest the trace.
   179  		trace, err := harvestTxnDataTrace(tr)
   180  		if err != nil {
   181  			t.Errorf("%s: error harvesting trace data: %v", tc.Name, err)
   182  		}
   183  
   184  		// Check the synthetics resource ID.
   185  		if trace.syntheticsResourceID != tc.OutputTransactionTrace.Header.Field9 {
   186  			t.Errorf("%s: unexpected field 9: expected=%s; got=%s", tc.Name, tc.OutputTransactionTrace.Header.Field9, trace.syntheticsResourceID)
   187  		}
   188  
   189  		// Check for expected intrinsics.
   190  		for key, value := range tc.OutputTransactionTrace.ExpectedIntrinsics {
   191  			// First, check if the key exists at all.
   192  			if !trace.traceDetails.attributes.intrinsics.has(key) {
   193  				t.Fatalf("%s: missing intrinsic %s", tc.Name, key)
   194  			}
   195  
   196  			// Everything we're looking for is a string, so we can be a little lazy
   197  			// here.
   198  			if err := trace.traceDetails.attributes.intrinsics.isString(key, value); err != nil {
   199  				t.Errorf("%s: %v", tc.Name, err)
   200  			}
   201  		}
   202  
   203  		// Now we verify that the unexpected intrinsics didn't miraculously appear.
   204  		for _, key := range tc.OutputTransactionTrace.NonExpectedIntrinsics {
   205  			if trace.traceDetails.attributes.intrinsics.has(key) {
   206  				t.Errorf("%s: expected intrinsic %s to be missing; instead, got value %v", tc.Name, key, trace.traceDetails.attributes.intrinsics[key])
   207  			}
   208  		}
   209  
   210  		// Harvest the event.
   211  		event, err := harvestTxnDataEvent(tr)
   212  		if err != nil {
   213  			t.Errorf("%s: error harvesting event data: %v", tc.Name, err)
   214  		}
   215  
   216  		// Now we have the event, let's look for the expected intrinsics.
   217  		for key, value := range tc.OutputTransactionEvent.ExpectedAttributes {
   218  			// First, check if the key exists at all.
   219  			if !event.intrinsics.has(key) {
   220  				t.Fatalf("%s: missing intrinsic %s", tc.Name, key)
   221  			}
   222  
   223  			// Everything we're looking for is a string, so we can be a little lazy
   224  			// here.
   225  			if err := event.intrinsics.isString(key, value); err != nil {
   226  				t.Errorf("%s: %v", tc.Name, err)
   227  			}
   228  		}
   229  
   230  		// Now we verify that the unexpected intrinsics didn't miraculously appear.
   231  		for _, key := range tc.OutputTransactionEvent.NonExpectedAttributes {
   232  			if event.intrinsics.has(key) {
   233  				t.Errorf("%s: expected intrinsic %s to be missing; instead, got value %v", tc.Name, key, event.intrinsics[key])
   234  			}
   235  		}
   236  	}
   237  }