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  }