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 }