github.com/waldiirawan/apm-agent-go/v2@v2.2.2/transport/http_test.go (about) 1 // Licensed to Elasticsearch B.V. under one or more contributor 2 // license agreements. See the NOTICE file distributed with 3 // this work for additional information regarding copyright 4 // ownership. Elasticsearch B.V. licenses this file to you under 5 // the Apache License, Version 2.0 (the "License"); you may 6 // not use this file except in compliance with the License. 7 // You may obtain a copy of the License at 8 // 9 // http://www.apache.org/licenses/LICENSE-2.0 10 // 11 // Unless required by applicable law or agreed to in writing, 12 // software distributed under the License is distributed on an 13 // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 // KIND, either express or implied. See the License for the 15 // specific language governing permissions and limitations 16 // under the License. 17 18 package transport_test 19 20 import ( 21 "context" 22 "crypto/tls" 23 "crypto/x509" 24 "encoding/pem" 25 "fmt" 26 "io" 27 "io/ioutil" 28 "log" 29 "net" 30 "net/http" 31 "net/http/httptest" 32 "net/textproto" 33 "net/url" 34 "os" 35 "strings" 36 "sync" 37 "sync/atomic" 38 "testing" 39 "time" 40 41 "github.com/pkg/errors" 42 "github.com/stretchr/testify/assert" 43 "github.com/stretchr/testify/require" 44 45 "github.com/waldiirawan/apm-agent-go/v2/apmconfig" 46 "github.com/waldiirawan/apm-agent-go/v2/transport" 47 ) 48 49 func init() { 50 // Don't let the environment influence tests. 51 os.Unsetenv("ELASTIC_APM_SERVER_TIMEOUT") 52 os.Unsetenv("ELASTIC_APM_SERVER_URLS") 53 os.Unsetenv("ELASTIC_APM_SERVER_URL") 54 os.Unsetenv("ELASTIC_APM_SECRET_TOKEN") 55 os.Unsetenv("ELASTIC_APM_SERVER_CERT") 56 os.Unsetenv("ELASTIC_APM_VERIFY_SERVER_CERT") 57 } 58 59 func TestNewHTTPTransportDefaultURL(t *testing.T) { 60 var h recordingHandler 61 server := httptest.NewUnstartedServer(&h) 62 defer server.Close() 63 64 lis, err := net.Listen("tcp", "localhost:8200") 65 if err != nil { 66 t.Skipf("cannot listen on default server address: %s", err) 67 } 68 server.Listener.Close() 69 server.Listener = lis 70 server.Start() 71 72 transport, err := transport.NewHTTPTransport(transport.HTTPTransportOptions{}) 73 assert.NoError(t, err) 74 err = transport.SendStream(context.Background(), strings.NewReader("")) 75 assert.NoError(t, err) 76 assert.Len(t, h.requests, 1) 77 } 78 79 func TestHTTPTransportUserAgent(t *testing.T) { 80 var h recordingHandler 81 server := httptest.NewServer(&h) 82 defer server.Close() 83 defer patchEnv("ELASTIC_APM_SERVER_URL", server.URL)() 84 85 transport, err := transport.NewHTTPTransport(transport.HTTPTransportOptions{}) 86 assert.NoError(t, err) 87 err = transport.SendStream(context.Background(), strings.NewReader("")) 88 assert.NoError(t, err) 89 assert.Len(t, h.requests, 1) 90 91 transport.SetUserAgent("foo") 92 err = transport.SendStream(context.Background(), strings.NewReader("")) 93 assert.NoError(t, err) 94 assert.Len(t, h.requests, 2) 95 96 assert.Regexp(t, "apm-agent-go/.*", h.requests[0].UserAgent()) 97 assert.Equal(t, "foo", h.requests[1].UserAgent()) 98 } 99 100 func TestHTTPTransportSecretToken(t *testing.T) { 101 var h recordingHandler 102 server := httptest.NewServer(&h) 103 defer server.Close() 104 defer patchEnv("ELASTIC_APM_SERVER_URL", server.URL)() 105 106 transport, err := transport.NewHTTPTransport(transport.HTTPTransportOptions{}) 107 transport.SetSecretToken("hunter2") 108 assert.NoError(t, err) 109 transport.SendStream(context.Background(), strings.NewReader("")) 110 111 assert.Len(t, h.requests, 1) 112 assertAuthorization(t, h.requests[0], "Bearer hunter2") 113 } 114 115 func TestHTTPTransportEnvSecretToken(t *testing.T) { 116 var h recordingHandler 117 server := httptest.NewServer(&h) 118 defer server.Close() 119 defer patchEnv("ELASTIC_APM_SERVER_URL", server.URL)() 120 defer patchEnv("ELASTIC_APM_SECRET_TOKEN", "hunter2")() 121 122 transport, err := transport.NewHTTPTransport(transport.HTTPTransportOptions{}) 123 assert.NoError(t, err) 124 transport.SendStream(context.Background(), strings.NewReader("")) 125 126 assert.Len(t, h.requests, 1) 127 assertAuthorization(t, h.requests[0], "Bearer hunter2") 128 } 129 130 func TestHTTPTransportAPIKey(t *testing.T) { 131 var h recordingHandler 132 server := httptest.NewServer(&h) 133 defer server.Close() 134 defer patchEnv("ELASTIC_APM_SERVER_URL", server.URL)() 135 136 transport, err := transport.NewHTTPTransport(transport.HTTPTransportOptions{}) 137 transport.SetAPIKey("hunter2") 138 assert.NoError(t, err) 139 transport.SendStream(context.Background(), strings.NewReader("")) 140 141 assert.Len(t, h.requests, 1) 142 assertAuthorization(t, h.requests[0], "ApiKey hunter2") 143 } 144 145 func TestHTTPTransportEnvAPIKey(t *testing.T) { 146 var h recordingHandler 147 server := httptest.NewServer(&h) 148 defer server.Close() 149 defer patchEnv("ELASTIC_APM_SERVER_URL", server.URL)() 150 defer patchEnv("ELASTIC_APM_API_KEY", "api_key_wins")() 151 defer patchEnv("ELASTIC_APM_SECRET_TOKEN", "secret_token_loses")() 152 153 transport, err := transport.NewHTTPTransport(transport.HTTPTransportOptions{}) 154 assert.NoError(t, err) 155 transport.SendStream(context.Background(), strings.NewReader("")) 156 157 assert.Len(t, h.requests, 1) 158 assertAuthorization(t, h.requests[0], "ApiKey api_key_wins") 159 } 160 161 func TestHTTPTransportNoAuthorization(t *testing.T) { 162 var h recordingHandler 163 transport, server := newHTTPTransport(t, &h) 164 defer server.Close() 165 166 transport.SendStream(context.Background(), strings.NewReader("")) 167 168 assert.Len(t, h.requests, 1) 169 assertAuthorization(t, h.requests[0]) 170 } 171 172 func TestHTTPTransportTLS(t *testing.T) { 173 var h recordingHandler 174 server := httptest.NewUnstartedServer(&h) 175 server.Config.ErrorLog = log.New(ioutil.Discard, "", 0) 176 server.StartTLS() 177 defer server.Close() 178 defer patchEnv("ELASTIC_APM_SERVER_URL", server.URL)() 179 180 transport, err := transport.NewHTTPTransport(transport.HTTPTransportOptions{}) 181 assert.NoError(t, err) 182 183 p := strings.NewReader("") 184 185 // Send should fail, because we haven't told the client 186 // about the CA certificate, nor configured it to disable 187 // certificate verification. 188 err = transport.SendStream(context.Background(), p) 189 assert.Error(t, err) 190 191 // Reconfigure the transport so that it knows about the 192 // CA certificate. We avoid using server.Client here, as 193 // it is not available in older versions of Go. 194 certificate, err := x509.ParseCertificate(server.TLS.Certificates[0].Certificate[0]) 195 assert.NoError(t, err) 196 certpool := x509.NewCertPool() 197 certpool.AddCert(certificate) 198 transport.Client.Transport = &http.Transport{ 199 TLSClientConfig: &tls.Config{ 200 RootCAs: certpool, 201 }, 202 } 203 err = transport.SendStream(context.Background(), p) 204 assert.NoError(t, err) 205 } 206 207 func TestHTTPTransportEnvVerifyServerCert(t *testing.T) { 208 var h recordingHandler 209 server := httptest.NewTLSServer(&h) 210 defer server.Close() 211 defer patchEnv("ELASTIC_APM_SERVER_URL", server.URL)() 212 defer patchEnv("ELASTIC_APM_VERIFY_SERVER_CERT", "false")() 213 214 transport, err := transport.NewHTTPTransport(transport.HTTPTransportOptions{}) 215 assert.NoError(t, err) 216 217 assert.NotNil(t, transport.Client) 218 assert.IsType(t, &http.Transport{}, transport.Client.Transport) 219 httpTransport := transport.Client.Transport.(*http.Transport) 220 assert.NotNil(t, httpTransport.TLSClientConfig) 221 assert.True(t, httpTransport.TLSClientConfig.InsecureSkipVerify) 222 223 err = transport.SendStream(context.Background(), strings.NewReader("")) 224 assert.NoError(t, err) 225 } 226 227 func TestHTTPError(t *testing.T) { 228 h := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 229 http.Error(w, "error-message", http.StatusInternalServerError) 230 }) 231 tr, server := newHTTPTransport(t, h) 232 defer server.Close() 233 234 err := tr.SendStream(context.Background(), strings.NewReader("")) 235 assert.EqualError(t, err, "request failed with 500 Internal Server Error: error-message") 236 } 237 238 func TestHTTPTransportContent(t *testing.T) { 239 var h recordingHandler 240 server := httptest.NewServer(&h) 241 defer server.Close() 242 defer patchEnv("ELASTIC_APM_SERVER_URL", server.URL)() 243 244 transport, err := transport.NewHTTPTransport(transport.HTTPTransportOptions{}) 245 assert.NoError(t, err) 246 transport.SendStream(context.Background(), strings.NewReader("request-body")) 247 248 require.Len(t, h.requests, 1) 249 assert.Equal(t, "deflate", h.requests[0].Header.Get("Content-Encoding")) 250 assert.Equal(t, "application/x-ndjson", h.requests[0].Header.Get("Content-Type")) 251 } 252 253 func TestHTTPTransportServerTimeout(t *testing.T) { 254 done := make(chan struct{}) 255 blockingHandler := func(w http.ResponseWriter, req *http.Request) { <-done } 256 server := httptest.NewServer(http.HandlerFunc(blockingHandler)) 257 defer server.Close() 258 defer close(done) 259 defer patchEnv("ELASTIC_APM_SERVER_URL", server.URL)() 260 defer patchEnv("ELASTIC_APM_SERVER_TIMEOUT", "50ms")() 261 262 before := time.Now() 263 transport, err := transport.NewHTTPTransport(transport.HTTPTransportOptions{}) 264 assert.NoError(t, err) 265 err = transport.SendStream(context.Background(), strings.NewReader("")) 266 taken := time.Since(before) 267 assert.Error(t, err) 268 err = errors.Cause(err) 269 assert.Implements(t, new(net.Error), err) 270 assert.True(t, err.(net.Error).Timeout()) 271 assert.Condition(t, func() bool { 272 return taken >= 50*time.Millisecond 273 }) 274 } 275 276 func TestHTTPTransportServerFailover(t *testing.T) { 277 defer patchEnv("ELASTIC_APM_VERIFY_SERVER_CERT", "false")() 278 279 var hosts []string 280 errorHandler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 281 hosts = append(hosts, req.Host) 282 http.Error(w, "error-message", http.StatusInternalServerError) 283 }) 284 server1 := httptest.NewServer(errorHandler) 285 defer server1.Close() 286 server2 := httptest.NewTLSServer(errorHandler) 287 defer server2.Close() 288 289 transport, err := transport.NewHTTPTransport(transport.HTTPTransportOptions{}) 290 require.NoError(t, err) 291 transport.SetServerURL(mustParseURL(server1.URL), mustParseURL(server2.URL)) 292 293 for i := 0; i < 4; i++ { 294 err := transport.SendStream(context.Background(), strings.NewReader("")) 295 assert.EqualError(t, err, "request failed with 500 Internal Server Error: error-message") 296 } 297 assert.Len(t, hosts, 4) 298 299 // Each time SendStream returns an error, the transport should switch 300 // to the next URL in the list. The list is shuffled so we only compare 301 // the output values to each other, rather than to the original input. 302 assert.NotEqual(t, hosts[0], hosts[1]) 303 assert.Equal(t, hosts[0], hosts[2]) 304 assert.Equal(t, hosts[1], hosts[3]) 305 } 306 307 func TestHTTPTransportV2NotFound(t *testing.T) { 308 server := httptest.NewServer(http.NotFoundHandler()) 309 defer server.Close() 310 311 transport, err := transport.NewHTTPTransport(transport.HTTPTransportOptions{}) 312 require.NoError(t, err) 313 transport.SetServerURL(mustParseURL(server.URL)) 314 315 err = transport.SendStream(context.Background(), strings.NewReader("")) 316 assert.EqualError(t, err, fmt.Sprintf("request failed with 404 Not Found: %s/intake/v2/events not found (requires APM Server 6.5.0 or newer)", server.URL)) 317 } 318 319 func TestHTTPTransportCACert(t *testing.T) { 320 var h recordingHandler 321 server := httptest.NewUnstartedServer(&h) 322 server.Config.ErrorLog = log.New(ioutil.Discard, "", 0) 323 server.StartTLS() 324 defer server.Close() 325 defer patchEnv("ELASTIC_APM_SERVER_URL", server.URL)() 326 327 p := strings.NewReader("") 328 329 // SendStream should fail, because we haven't told the client about 330 // the server certificate, nor disabled certificate verification. 331 trans, err := transport.NewHTTPTransport(transport.HTTPTransportOptions{}) 332 assert.NoError(t, err) 333 assert.NotNil(t, trans) 334 err = trans.SendStream(context.Background(), p) 335 assert.Error(t, err) 336 337 // Set the env var to a file that doesn't exist, should get an error 338 defer patchEnv("ELASTIC_APM_SERVER_CA_CERT_FILE", "./testdata/file_that_doesnt_exist.pem")() 339 trans, err = transport.NewHTTPTransport(transport.HTTPTransportOptions{}) 340 assert.Error(t, err) 341 assert.Nil(t, trans) 342 343 // Set the env var to a file that has no cert, should get an error 344 f, err := ioutil.TempFile("", "apm-test-1") 345 require.NoError(t, err) 346 defer os.Remove(f.Name()) 347 defer f.Close() 348 defer patchEnv("ELASTIC_APM_SERVER_CA_CERT_FILE", f.Name())() 349 trans, err = transport.NewHTTPTransport(transport.HTTPTransportOptions{}) 350 assert.Error(t, err) 351 assert.Nil(t, trans) 352 353 // Set a certificate that doesn't match, SendStream should still fail 354 defer patchEnv("ELASTIC_APM_SERVER_CA_CERT_FILE", "./testdata/cert.pem")() 355 trans, err = transport.NewHTTPTransport(transport.HTTPTransportOptions{}) 356 assert.NoError(t, err) 357 assert.NotNil(t, trans) 358 err = trans.SendStream(context.Background(), p) 359 assert.Error(t, err) 360 361 f, err = ioutil.TempFile("", "apm-test-2") 362 require.NoError(t, err) 363 defer os.Remove(f.Name()) 364 defer f.Close() 365 defer patchEnv("ELASTIC_APM_SERVER_CA_CERT_FILE", f.Name())() 366 367 err = pem.Encode(f, &pem.Block{ 368 Type: "CERTIFICATE", 369 Bytes: server.TLS.Certificates[0].Certificate[0], 370 }) 371 require.NoError(t, err) 372 373 trans, err = transport.NewHTTPTransport(transport.HTTPTransportOptions{}) 374 assert.NoError(t, err) 375 assert.NotNil(t, trans) 376 err = trans.SendStream(context.Background(), p) 377 assert.NoError(t, err) 378 } 379 380 func TestHTTPTransportServerCert(t *testing.T) { 381 var h recordingHandler 382 server := httptest.NewUnstartedServer(&h) 383 server.Config.ErrorLog = log.New(ioutil.Discard, "", 0) 384 server.StartTLS() 385 defer server.Close() 386 defer patchEnv("ELASTIC_APM_SERVER_URL", server.URL)() 387 388 p := strings.NewReader("") 389 390 newTransport := func() *transport.HTTPTransport { 391 transport, err := transport.NewHTTPTransport(transport.HTTPTransportOptions{}) 392 require.NoError(t, err) 393 return transport 394 } 395 396 // SendStream should fail, because we haven't told the client about 397 // the server certificate, nor disabled certificate verification. 398 transport := newTransport() 399 err := transport.SendStream(context.Background(), p) 400 assert.Error(t, err) 401 402 // Set a certificate that doesn't match, SendStream should still fail. 403 defer patchEnv("ELASTIC_APM_SERVER_CERT", "./testdata/cert.pem")() 404 transport = newTransport() 405 err = transport.SendStream(context.Background(), p) 406 assert.Error(t, err) 407 408 f, err := ioutil.TempFile("", "apm-test") 409 require.NoError(t, err) 410 defer os.Remove(f.Name()) 411 defer f.Close() 412 defer patchEnv("ELASTIC_APM_SERVER_CERT", f.Name())() 413 414 // Reconfigure the transport so that it knows about the 415 // server certificate. We avoid using server.Client here, as 416 // it is not available in older versions of Go. 417 err = pem.Encode(f, &pem.Block{ 418 Type: "CERTIFICATE", 419 Bytes: server.TLS.Certificates[0].Certificate[0], 420 }) 421 require.NoError(t, err) 422 423 transport = newTransport() 424 err = transport.SendStream(context.Background(), p) 425 assert.NoError(t, err) 426 } 427 428 func TestHTTPTransportServerCertInvalid(t *testing.T) { 429 f, err := ioutil.TempFile("", "apm-test") 430 require.NoError(t, err) 431 defer os.Remove(f.Name()) 432 defer f.Close() 433 defer patchEnv("ELASTIC_APM_SERVER_CERT", f.Name())() 434 435 fmt.Fprintln(f, ` 436 -----BEGIN GARBAGE----- 437 garbage 438 -----END GARBAGE----- 439 `[1:]) 440 441 _, err = transport.NewHTTPTransport(transport.HTTPTransportOptions{}) 442 assert.EqualError(t, err, fmt.Sprintf("failed to load certificate from %s: missing or invalid certificate", f.Name())) 443 } 444 445 func TestHTTPTransportWatchConfig(t *testing.T) { 446 type response struct { 447 code int 448 cacheControl string 449 etag string 450 body string 451 } 452 responses := make(chan response, 1) 453 454 var responseEtag string 455 transport, server := newHTTPTransport(t, http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 456 var response response 457 var ok bool 458 select { 459 case response, ok = <-responses: 460 if !ok { 461 w.WriteHeader(http.StatusTeapot) 462 return 463 } 464 case <-time.After(10 * time.Millisecond): 465 // This is necessary in case the previous config change 466 // wasn't consumed before a new request was made. This 467 // will return to the request loop. 468 w.Header().Set("Cache-Control", "max-age=0") 469 w.WriteHeader(http.StatusNotModified) 470 return 471 case <-req.Context().Done(): 472 return 473 } 474 ifNoneMatch := req.Header.Get("If-None-Match") 475 if ifNoneMatch == "" { 476 assert.Equal(t, "", responseEtag) 477 } else { 478 assert.Equal(t, responseEtag, ifNoneMatch) 479 } 480 if response.cacheControl != "" { 481 w.Header().Set("Cache-Control", response.cacheControl) 482 } 483 if response.etag != "" { 484 w.Header().Set("Etag", response.etag) 485 responseEtag = response.etag 486 } 487 w.WriteHeader(response.code) 488 w.Write([]byte(response.body)) 489 })) 490 defer server.Close() 491 492 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 493 defer cancel() 494 495 var watchParams apmconfig.WatchParams 496 watchParams.Service.Name = "name" 497 watchParams.Service.Environment = "env" 498 changes := transport.WatchConfig(ctx, watchParams) 499 require.NotNil(t, changes) 500 501 responses <- response{code: 200, cacheControl: "max-age=0", etag: `"empty"`} 502 assert.Equal(t, apmconfig.Change{Attrs: map[string]string{}}, <-changes) 503 504 responses <- response{code: 200, cacheControl: "max-age=0", etag: `"foobar"`, body: `{"foo": "bar"}`} 505 assert.Equal(t, apmconfig.Change{Attrs: map[string]string{"foo": "bar"}}, <-changes) 506 507 responses <- response{code: 200, cacheControl: "max-age=0", etag: `"empty"`} 508 assert.Equal(t, apmconfig.Change{Attrs: map[string]string{}}, <-changes) 509 510 responses <- response{code: 304, cacheControl: "max-age=0"} 511 // No change. 512 513 responses <- response{code: 200, cacheControl: "max-age=0", etag: `"foobaz"`, body: `{"foo": "baz"}`} 514 assert.Equal(t, apmconfig.Change{Attrs: map[string]string{"foo": "baz"}}, <-changes) 515 516 responses <- response{code: 200, cacheControl: "max-age=0", etag: `"foobar"`, body: `{"foo": "bar"}`} 517 assert.Equal(t, apmconfig.Change{Attrs: map[string]string{"foo": "bar"}}, <-changes) 518 519 responses <- response{code: 403, cacheControl: "max-age=0"} 520 // 403s are not reported. 521 522 close(responses) 523 if change := <-changes; assert.Error(t, change.Err) { 524 assert.Equal(t, "request failed with 418 I'm a teapot", change.Err.Error()) 525 } 526 } 527 528 func TestHTTPTransportWatchConfigQueryParams(t *testing.T) { 529 test := func(t *testing.T, serviceName, serviceEnvironment, expectedQuery string) { 530 query, err := url.ParseQuery(expectedQuery) 531 require.NoError(t, err) 532 transport, server := newHTTPTransport(t, http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 533 assert.Equal(t, query, req.URL.Query()) 534 w.WriteHeader(500) 535 })) 536 defer server.Close() 537 538 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 539 defer cancel() 540 541 var watchParams apmconfig.WatchParams 542 watchParams.Service.Name = serviceName 543 watchParams.Service.Environment = serviceEnvironment 544 <-transport.WatchConfig(ctx, watchParams) 545 } 546 t.Run("name_only", func(t *testing.T) { test(t, "opbeans", "", "service.name=opbeans") }) 547 t.Run("name_and_env", func(t *testing.T) { test(t, "opbeans", "dev", "service.name=opbeans&service.environment=dev") }) 548 t.Run("name_empty", func(t *testing.T) { test(t, "", "dev", "service.name=&service.environment=dev") }) 549 t.Run("both_empty", func(t *testing.T) { test(t, "", "", "service.name=") }) 550 } 551 552 func TestHTTPTransportWatchConfigContextCancelled(t *testing.T) { 553 ctx, cancel := context.WithCancel(context.Background()) 554 defer cancel() 555 556 transport, server := newHTTPTransport(t, http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 557 cancel() // cancel client-side request context 558 <-req.Context().Done() 559 })) 560 defer server.Close() 561 562 var watchParams apmconfig.WatchParams 563 watchParams.Service.Name = "name" 564 watchParams.Service.Environment = "env" 565 changes := transport.WatchConfig(ctx, watchParams) 566 require.NotNil(t, changes) 567 568 _, ok := <-changes 569 require.False(t, ok) 570 } 571 572 func TestNewHTTPTransportTrailingSlash(t *testing.T) { 573 var h recordingHandler 574 mux := http.NewServeMux() 575 mux.Handle("/intake/v2/events", &h) 576 transport, server := newHTTPTransport(t, mux) 577 defer server.Close() 578 579 transport.SetServerURL(mustParseURL(server.URL + "/")) 580 581 err := transport.SendStream(context.Background(), strings.NewReader("")) 582 assert.NoError(t, err) 583 require.Len(t, h.requests, 1) 584 assert.Equal(t, "POST", h.requests[0].Method) 585 assert.Equal(t, "/intake/v2/events", h.requests[0].URL.Path) 586 } 587 588 func TestHTTPTransportSendProfile(t *testing.T) { 589 metadata := "metadata" 590 profile1 := "profile1" 591 profile2 := "profile2" 592 593 type part struct { 594 formName string 595 fileName string 596 header textproto.MIMEHeader 597 content string 598 } 599 600 var parts []part 601 transport, server := newHTTPTransport(t, http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 602 r, err := req.MultipartReader() 603 if err != nil { 604 panic(err) 605 } 606 for { 607 p, err := r.NextPart() 608 if err == io.EOF { 609 break 610 } 611 content, err := ioutil.ReadAll(p) 612 if err == io.EOF { 613 panic(err) 614 } 615 parts = append(parts, part{ 616 formName: p.FormName(), 617 fileName: p.FileName(), 618 header: p.Header, 619 content: string(content), 620 }) 621 } 622 })) 623 defer server.Close() 624 625 err := transport.SendProfile( 626 context.Background(), 627 strings.NewReader(metadata), 628 strings.NewReader(profile1), 629 strings.NewReader(profile2), 630 ) 631 require.NoError(t, err) 632 633 makeHeader := func(kv ...string) textproto.MIMEHeader { 634 h := make(textproto.MIMEHeader) 635 for i := 0; i < len(kv); i += 2 { 636 h.Set(kv[i], kv[i+1]) 637 } 638 return h 639 } 640 641 assert.Equal(t, 642 []part{{ 643 formName: "metadata", 644 header: makeHeader( 645 "Content-Disposition", `form-data; name="metadata"`, 646 "Content-Type", "application/json", 647 ), 648 content: "metadata", 649 }, { 650 formName: "profile", 651 header: makeHeader( 652 "Content-Disposition", `form-data; name="profile"`, 653 "Content-Type", `application/x-protobuf; messageType="perftools.profiles.Profile"`, 654 ), 655 content: "profile1", 656 }, { 657 formName: "profile", 658 header: makeHeader( 659 "Content-Disposition", `form-data; name="profile"`, 660 "Content-Type", `application/x-protobuf; messageType="perftools.profiles.Profile"`, 661 ), 662 content: "profile2", 663 }}, 664 parts, 665 ) 666 } 667 668 func TestHTTPTransportOptionsValidation(t *testing.T) { 669 validURL, err := url.Parse("http://localhost:8200") 670 require.NoError(t, err) 671 672 t.Run("valid", func(t *testing.T) { 673 transport, err := transport.NewHTTPTransport(transport.HTTPTransportOptions{ 674 ServerURLs: []*url.URL{validURL}, 675 ServerTimeout: 30 * time.Second, 676 }) 677 assert.NoError(t, err) 678 assert.NotNil(t, transport) 679 }) 680 t.Run("invalid_timeout", func(t *testing.T) { 681 transport, err := transport.NewHTTPTransport(transport.HTTPTransportOptions{ 682 ServerTimeout: -1, 683 }) 684 assert.EqualError(t, err, "apm transport options: ServerTimeout must be greater or equal to 0") 685 assert.Nil(t, transport) 686 }) 687 } 688 689 func TestHTTPTransportOptionsEmptyURL(t *testing.T) { 690 var h recordingHandler 691 server := httptest.NewUnstartedServer(&h) 692 defer server.Close() 693 694 lis, err := net.Listen("tcp", "localhost:8200") 695 if err != nil { 696 t.Skipf("cannot listen on default server address: %s", err) 697 } 698 server.Listener.Close() 699 server.Listener = lis 700 server.Start() 701 702 transport, err := transport.NewHTTPTransport(transport.HTTPTransportOptions{}) 703 require.NoError(t, err) 704 require.NotNil(t, transport) 705 706 err = transport.SendStream(context.Background(), strings.NewReader("")) 707 assert.NoError(t, err) 708 assert.Len(t, h.requests, 1) 709 } 710 711 func TestHTTPTransportOptionsDefaults(t *testing.T) { 712 validURL, err := url.Parse("http://localhost:8200") 713 require.NoError(t, err) 714 transport, err := transport.NewHTTPTransport(transport.HTTPTransportOptions{ 715 ServerURLs: []*url.URL{validURL}, 716 }) 717 assert.NoError(t, err) 718 assert.Equal(t, transport.Client.Timeout, 30*time.Second) 719 } 720 721 func TestSetServerURL(t *testing.T) { 722 t.Run("valid", func(t *testing.T) { 723 validURL, err := url.Parse("http://localhost:8200") 724 require.NoError(t, err) 725 transport, err := transport.NewHTTPTransport(transport.HTTPTransportOptions{ 726 ServerURLs: []*url.URL{validURL}, 727 }) 728 anotherURL, err := url.Parse("http://somethingelse:8200") 729 require.NoError(t, err) 730 731 err = transport.SetServerURL(anotherURL) 732 require.NoError(t, err) 733 }) 734 t.Run("invalid", func(t *testing.T) { 735 validURL, err := url.Parse("http://localhost:8200") 736 require.NoError(t, err) 737 transport, err := transport.NewHTTPTransport(transport.HTTPTransportOptions{ 738 ServerURLs: []*url.URL{validURL}, 739 }) 740 741 err = transport.SetServerURL() 742 require.EqualError(t, err, "SetServerURL expects at least one URL") 743 }) 744 } 745 746 func TestMajorServerVersion(t *testing.T) { 747 newTransport := func(t *testing.T, u string) *transport.HTTPTransport { 748 validURL, err := url.Parse(u) 749 require.NoError(t, err) 750 transport, err := transport.NewHTTPTransport(transport.HTTPTransportOptions{ 751 ServerURLs: []*url.URL{validURL}, 752 }) 753 require.NoError(t, err) 754 return transport 755 } 756 757 t.Run("failure", func(t *testing.T) { 758 var count uint32 759 srv := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) { 760 switch atomic.LoadUint32(&count) { 761 case 0: 762 rw.WriteHeader(200) 763 rw.Write([]byte(`invalid json`)) 764 case 1: 765 rw.WriteHeader(200) 766 rw.Write([]byte(`{"version":"7.17.0"}`)) 767 default: 768 http.Error(rw, `{"ok":false,"message":"The instance rejected the connection."}`, 502) 769 } 770 atomic.AddUint32(&count, 1) 771 })) 772 defer srv.Close() 773 774 transport := newTransport(t, srv.URL) 775 ctx, cancel := context.WithTimeout(context.Background(), time.Second) 776 defer cancel() 777 778 version := transport.MajorServerVersion(ctx, true) 779 assert.Zero(t, version) 780 781 version = transport.MajorServerVersion(ctx, true) 782 assert.Equal(t, uint32(7), version) 783 784 // Verifies that the cache has been invalidated when the server returns 785 // an error. 786 transport.SendStream(ctx, strings.NewReader("{}")) 787 version = transport.MajorServerVersion(ctx, false) 788 assert.Zero(t, version) 789 }) 790 t.Run("failure_timeout", func(t *testing.T) { 791 var count uint32 792 wait := make(chan struct{}) 793 srv := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) { 794 c := atomic.LoadUint32(&count) 795 atomic.AddUint32(&count, 1) 796 if c == 0 { 797 <-wait 798 return 799 } 800 rw.WriteHeader(200) 801 rw.Write([]byte(`{"version":"7.16.3"}`)) 802 })) 803 defer srv.Close() 804 805 transport := newTransport(t, srv.URL) 806 ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) 807 defer cancel() 808 version := transport.MajorServerVersion(ctx, true) 809 close(wait) 810 assert.Zero(t, version) 811 assert.Error(t, ctx.Err()) 812 813 version = transport.MajorServerVersion(context.Background(), true) 814 assert.Equal(t, uint32(2), count, "count == 1 means that the first request context was cancelled before the http test server received it") 815 assert.Equal(t, uint32(7), version) 816 }) 817 t.Run("success", func(t *testing.T) { 818 var count uint32 819 srv := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { 820 assert.Equal(t, "/", r.URL.Path) 821 rw.WriteHeader(200) 822 if atomic.LoadUint32(&count) > 0 { 823 rw.Write([]byte(`{"version":"8.1.0"}`)) 824 } else { 825 rw.Write([]byte(`{"version":"8.0.0"}`)) 826 } 827 atomic.AddUint32(&count, 1) 828 })) 829 defer srv.Close() 830 831 transport := newTransport(t, srv.URL) 832 ctx, cancel := context.WithTimeout(context.Background(), time.Second) 833 defer cancel() 834 835 // Run GetVersion a few times and ensure that the same version is 836 // returned on subsequent calls 837 for i := 0; i < 5; i++ { 838 version := transport.MajorServerVersion(ctx, true) 839 assert.Equal(t, uint32(8), version, fmt.Sprintf("iteration %d", i)) 840 } 841 }) 842 t.Run("concurrent", func(t *testing.T) { 843 var count uint32 844 srv := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { 845 rw.WriteHeader(200) 846 if atomic.LoadUint32(&count) > 0 { 847 rw.Write([]byte(`{"version":"8.1.0"}`)) 848 } else { 849 rw.Write([]byte(`{"version":"8.0.0"}`)) 850 } 851 atomic.AddUint32(&count, 1) 852 })) 853 defer srv.Close() 854 855 transport := newTransport(t, srv.URL) 856 ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) 857 defer cancel() 858 859 // Run GetVersion a few times and ensure that the same version is 860 // returned on subsequent calls 861 var wg sync.WaitGroup 862 iterations := 5 863 wg.Add(iterations) 864 for i := 0; i < iterations; i++ { 865 go func(i int) { 866 version := transport.MajorServerVersion(ctx, true) 867 assert.Equal(t, uint32(8), version, fmt.Sprintf("iteration %d", i)) 868 wg.Done() 869 }(i) 870 } 871 wg.Wait() 872 }) 873 } 874 875 func newHTTPTransport(t *testing.T, handler http.Handler) (*transport.HTTPTransport, *httptest.Server) { 876 server := httptest.NewServer(handler) 877 transport, err := transport.NewHTTPTransport(transport.HTTPTransportOptions{ 878 ServerURLs: []*url.URL{mustParseURL(server.URL)}, 879 }) 880 if !assert.NoError(t, err) { 881 server.Close() 882 t.FailNow() 883 } 884 return transport, server 885 } 886 887 func mustParseURL(s string) *url.URL { 888 url, err := url.Parse(s) 889 if err != nil { 890 panic(err) 891 } 892 return url 893 }