github.com/lulzWill/go-agent@v2.1.2+incompatible/internal/synthetics_test.go (about)

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