github.com/google/cloudprober@v0.11.3/probes/http/http_test.go (about)

     1  // Copyright 2017-2019 The Cloudprober Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package http
    16  
    17  import (
    18  	"bytes"
    19  	"context"
    20  	"errors"
    21  	"fmt"
    22  	"io"
    23  	"io/ioutil"
    24  	"net/http"
    25  	"strings"
    26  	"sync"
    27  	"testing"
    28  	"time"
    29  
    30  	"github.com/golang/protobuf/proto"
    31  	"github.com/google/cloudprober/metrics"
    32  	"github.com/google/cloudprober/metrics/testutils"
    33  	configpb "github.com/google/cloudprober/probes/http/proto"
    34  	"github.com/google/cloudprober/probes/options"
    35  	"github.com/google/cloudprober/targets"
    36  	"github.com/google/cloudprober/targets/endpoint"
    37  )
    38  
    39  // The Transport is mocked instead of the Client because Client is not an
    40  // interface, but RoundTripper (which Transport implements) is.
    41  type testTransport struct {
    42  	noBody                   io.ReadCloser
    43  	lastProcessedRequestBody []byte
    44  }
    45  
    46  func newTestTransport() *testTransport {
    47  	return &testTransport{}
    48  }
    49  
    50  // This mocks the Body of an http.Response.
    51  type testReadCloser struct {
    52  	b *bytes.Buffer
    53  }
    54  
    55  func (trc *testReadCloser) Read(p []byte) (n int, err error) {
    56  	return trc.b.Read(p)
    57  }
    58  
    59  func (trc *testReadCloser) Close() error {
    60  	return nil
    61  }
    62  
    63  func (tt *testTransport) RoundTrip(req *http.Request) (*http.Response, error) {
    64  	if req.URL.Host == "fail-test.com" {
    65  		return nil, errors.New("failing for fail-target.com")
    66  	}
    67  
    68  	if req.Body == nil {
    69  		return &http.Response{Body: http.NoBody}, nil
    70  	}
    71  
    72  	b, err := ioutil.ReadAll(req.Body)
    73  	if err != nil {
    74  		return nil, err
    75  	}
    76  	req.Body.Close()
    77  	tt.lastProcessedRequestBody = b
    78  
    79  	return &http.Response{
    80  		Body: &testReadCloser{
    81  			b: bytes.NewBuffer(b),
    82  		},
    83  	}, nil
    84  }
    85  
    86  func (tt *testTransport) CancelRequest(req *http.Request) {}
    87  
    88  func testProbe(opts *options.Options) (*probeResult, error) {
    89  	p := &Probe{}
    90  	err := p.Init("http_test", opts)
    91  	if err != nil {
    92  		return nil, err
    93  	}
    94  	p.client.Transport = newTestTransport()
    95  
    96  	target := endpoint.Endpoint{Name: "test.com"}
    97  	result := p.newResult()
    98  	req := p.httpRequestForTarget(target, nil)
    99  	p.runProbe(context.Background(), target, req, result)
   100  
   101  	return result, nil
   102  }
   103  
   104  func TestProbeVariousMethods(t *testing.T) {
   105  	mpb := func(s string) *configpb.ProbeConf_Method {
   106  		return configpb.ProbeConf_Method(configpb.ProbeConf_Method_value[s]).Enum()
   107  	}
   108  
   109  	testBody := "Test HTTP Body"
   110  	testHeaderName, testHeaderValue := "Content-Type", "application/json"
   111  
   112  	var tests = []struct {
   113  		input *configpb.ProbeConf
   114  		want  string
   115  	}{
   116  		{&configpb.ProbeConf{}, "total: 1, success: 1"},
   117  		{&configpb.ProbeConf{Protocol: configpb.ProbeConf_HTTPS.Enum()}, "total: 1, success: 1"},
   118  		{&configpb.ProbeConf{RequestsPerProbe: proto.Int32(1)}, "total: 1, success: 1"},
   119  		{&configpb.ProbeConf{RequestsPerProbe: proto.Int32(4)}, "total: 4, success: 4"},
   120  		{&configpb.ProbeConf{Method: mpb("GET")}, "total: 1, success: 1"},
   121  		{&configpb.ProbeConf{Method: mpb("POST")}, "total: 1, success: 1"},
   122  		{&configpb.ProbeConf{Method: mpb("POST"), Body: &testBody}, "total: 1, success: 1"},
   123  		{&configpb.ProbeConf{Method: mpb("PUT")}, "total: 1, success: 1"},
   124  		{&configpb.ProbeConf{Method: mpb("PUT"), Body: &testBody}, "total: 1, success: 1"},
   125  		{&configpb.ProbeConf{Method: mpb("HEAD")}, "total: 1, success: 1"},
   126  		{&configpb.ProbeConf{Method: mpb("DELETE")}, "total: 1, success: 1"},
   127  		{&configpb.ProbeConf{Method: mpb("PATCH")}, "total: 1, success: 1"},
   128  		{&configpb.ProbeConf{Method: mpb("OPTIONS")}, "total: 1, success: 1"},
   129  		{&configpb.ProbeConf{Headers: []*configpb.ProbeConf_Header{{Name: &testHeaderName, Value: &testHeaderValue}}}, "total: 1, success: 1"},
   130  	}
   131  
   132  	for i, test := range tests {
   133  		t.Run(fmt.Sprintf("Test_case(%d)_config(%v)", i, test.input), func(t *testing.T) {
   134  			opts := &options.Options{
   135  				Targets:   targets.StaticTargets("test.com"),
   136  				Interval:  2 * time.Second,
   137  				Timeout:   time.Second,
   138  				ProbeConf: test.input,
   139  			}
   140  
   141  			result, err := testProbe(opts)
   142  			if err != nil {
   143  				if fmt.Sprintf("error: '%s'", err.Error()) != test.want {
   144  					t.Errorf("Unexpected initialization error: %v", err)
   145  				}
   146  				return
   147  			}
   148  
   149  			got := fmt.Sprintf("total: %d, success: %d", result.total, result.success)
   150  			if got != test.want {
   151  				t.Errorf("Mismatch got '%s', want '%s'", got, test.want)
   152  			}
   153  		})
   154  	}
   155  }
   156  
   157  func TestProbeWithBody(t *testing.T) {
   158  	testBody := "TestHTTPBody"
   159  	testTarget := "test.com"
   160  	// Build the expected response code map
   161  	expectedMap := metrics.NewMap("resp", metrics.NewInt(0))
   162  	expectedMap.IncKey(testBody)
   163  	expected := expectedMap.String()
   164  
   165  	p := &Probe{}
   166  	err := p.Init("http_test", &options.Options{
   167  		Targets:  targets.StaticTargets(testTarget),
   168  		Interval: 2 * time.Second,
   169  		ProbeConf: &configpb.ProbeConf{
   170  			Body:                    &testBody,
   171  			ExportResponseAsMetrics: proto.Bool(true),
   172  		},
   173  	})
   174  
   175  	if err != nil {
   176  		t.Errorf("Error while initializing probe: %v", err)
   177  	}
   178  	p.client.Transport = newTestTransport()
   179  	target := endpoint.Endpoint{Name: testTarget}
   180  
   181  	// Probe 1st run
   182  	result := p.newResult()
   183  	req := p.httpRequestForTarget(target, nil)
   184  	p.runProbe(context.Background(), target, req, result)
   185  	got := result.respBodies.String()
   186  	if got != expected {
   187  		t.Errorf("response map: got=%s, expected=%s", got, expected)
   188  	}
   189  
   190  	// Probe 2nd run (we should get the same request body).
   191  	p.runProbe(context.Background(), target, req, result)
   192  	expectedMap.IncKey(testBody)
   193  	expected = expectedMap.String()
   194  	got = result.respBodies.String()
   195  	if got != expected {
   196  		t.Errorf("response map: got=%s, expected=%s", got, expected)
   197  	}
   198  }
   199  
   200  func TestProbeWithLargeBody(t *testing.T) {
   201  	for _, size := range []int{largeBodyThreshold - 1, largeBodyThreshold, largeBodyThreshold + 1, largeBodyThreshold * 2} {
   202  		t.Run(fmt.Sprintf("size:%d", size), func(t *testing.T) {
   203  			testProbeWithLargeBody(t, size)
   204  		})
   205  	}
   206  }
   207  
   208  func testProbeWithLargeBody(t *testing.T, bodySize int) {
   209  	testBody := strings.Repeat("a", bodySize)
   210  	testTarget := "test-large-body.com"
   211  
   212  	p := &Probe{}
   213  	err := p.Init("http_test", &options.Options{
   214  		Targets:  targets.StaticTargets(testTarget),
   215  		Interval: 2 * time.Second,
   216  		ProbeConf: &configpb.ProbeConf{
   217  			Body: &testBody,
   218  			// Can't use ExportResponseAsMetrics for large bodies,
   219  			// since maxResponseSizeForMetrics is small
   220  			ExportResponseAsMetrics: proto.Bool(false),
   221  		},
   222  	})
   223  
   224  	if err != nil {
   225  		t.Errorf("Error while initializing probe: %v", err)
   226  	}
   227  	testTransport := newTestTransport()
   228  	p.client.Transport = testTransport
   229  	target := endpoint.Endpoint{Name: testTarget}
   230  
   231  	// Probe 1st run
   232  	result := p.newResult()
   233  	req := p.httpRequestForTarget(target, nil)
   234  	p.runProbe(context.Background(), target, req, result)
   235  
   236  	got := string(testTransport.lastProcessedRequestBody)
   237  	if got != testBody {
   238  		t.Errorf("response body length: got=%d, expected=%d", len(got), len(testBody))
   239  	}
   240  
   241  	// Probe 2nd run (we should get the same request body).
   242  	p.runProbe(context.Background(), target, req, result)
   243  	got = string(testTransport.lastProcessedRequestBody)
   244  	if got != testBody {
   245  		t.Errorf("response body length: got=%d, expected=%d", len(got), len(testBody))
   246  	}
   247  }
   248  
   249  func TestMultipleTargetsMultipleRequests(t *testing.T) {
   250  	testTargets := []string{"test.com", "fail-test.com", "fails-to-resolve.com"}
   251  	reqPerProbe := int64(3)
   252  	opts := &options.Options{
   253  		Targets:             targets.StaticTargets(strings.Join(testTargets, ",")),
   254  		Interval:            10 * time.Millisecond,
   255  		StatsExportInterval: 20 * time.Millisecond,
   256  		ProbeConf:           &configpb.ProbeConf{RequestsPerProbe: proto.Int32(int32(reqPerProbe))},
   257  		LogMetrics:          func(_ *metrics.EventMetrics) {},
   258  	}
   259  
   260  	p := &Probe{}
   261  	err := p.Init("http_test", opts)
   262  	if err != nil {
   263  		t.Errorf("Unexpected error: %v", err)
   264  		return
   265  	}
   266  	p.client.Transport = newTestTransport()
   267  
   268  	ctx, cancelF := context.WithCancel(context.Background())
   269  	dataChan := make(chan *metrics.EventMetrics, 100)
   270  
   271  	var wg sync.WaitGroup
   272  	wg.Add(1)
   273  	go func() {
   274  		defer wg.Done()
   275  		p.Start(ctx, dataChan)
   276  	}()
   277  
   278  	// target -> [success, total]
   279  	wantData := map[string][2]int64{
   280  		"test.com": [2]int64{2 * reqPerProbe, 2 * reqPerProbe},
   281  
   282  		// Test transport is configured to fail this.
   283  		"fail-test.com": [2]int64{0, 2 * reqPerProbe},
   284  
   285  		// No probes sent because of bad target (http)
   286  		"fails-to-resolve.com": [2]int64{0, 0},
   287  	}
   288  
   289  	ems, err := testutils.MetricsFromChannel(dataChan, 100, time.Second)
   290  	// We should receive at least 4 eventmetrics: 2 probe cycle x 2 targets.
   291  	if err != nil && len(ems) < 4 {
   292  		t.Errorf("Error getting 4 eventmetrics from data channel: %v", err)
   293  	}
   294  
   295  	// Following verifies that we are able to cleanly stop the probe.
   296  	cancelF()
   297  	wg.Wait()
   298  
   299  	dataMap := testutils.MetricsMap(ems)
   300  	for tgt, d := range wantData {
   301  		wantSuccessVal, wantTotalVal := d[0], d[1]
   302  		successVals, totalVals := dataMap["success"][tgt], dataMap["total"][tgt]
   303  
   304  		if len(successVals) < 1 {
   305  			t.Errorf("Success metric for %s: %v (less than 1)", tgt, successVals)
   306  			continue
   307  		}
   308  		latestVal := successVals[len(successVals)-1].Metric("success").(*metrics.Int).Int64()
   309  		if latestVal < wantSuccessVal {
   310  			t.Errorf("Got success value for target (%s): %d, want: %d", tgt, latestVal, wantSuccessVal)
   311  		}
   312  
   313  		if len(totalVals) < 1 {
   314  			t.Errorf("Total metric for %s: %v (less than 1)", tgt, totalVals)
   315  			continue
   316  		}
   317  		latestVal = totalVals[len(totalVals)-1].Metric("total").(*metrics.Int).Int64()
   318  		if latestVal < wantTotalVal {
   319  			t.Errorf("Got total value for target (%s): %d, want: %d", tgt, latestVal, wantTotalVal)
   320  		}
   321  	}
   322  }
   323  
   324  func compareNumberOfMetrics(t *testing.T, ems []*metrics.EventMetrics, targets [2]string, wantCloseRange bool) {
   325  	t.Helper()
   326  
   327  	m := testutils.MetricsMap(ems)["success"]
   328  	num1 := len(m[targets[0]])
   329  	num2 := len(m[targets[1]])
   330  
   331  	diff := num1 - num2
   332  	threshold := num1 / 2
   333  	notCloseRange := diff < -(threshold) || diff > threshold
   334  
   335  	if notCloseRange && wantCloseRange {
   336  		t.Errorf("Number of metrics for two targets are not within a close range (%d, %d)", num1, num2)
   337  	}
   338  	if !notCloseRange && !wantCloseRange {
   339  		t.Errorf("Number of metrics for two targets are within a close range (%d, %d)", num1, num2)
   340  	}
   341  }
   342  
   343  func TestUpdateTargetsAndStartProbes(t *testing.T) {
   344  	testTargets := [2]string{"test1.com", "test2.com"}
   345  	reqPerProbe := int64(3)
   346  	opts := &options.Options{
   347  		Targets:             targets.StaticTargets(fmt.Sprintf("%s,%s", testTargets[0], testTargets[1])),
   348  		Interval:            10 * time.Millisecond,
   349  		StatsExportInterval: 20 * time.Millisecond,
   350  		ProbeConf:           &configpb.ProbeConf{RequestsPerProbe: proto.Int32(int32(reqPerProbe))},
   351  		LogMetrics:          func(_ *metrics.EventMetrics) {},
   352  	}
   353  	p := &Probe{}
   354  	p.Init("http_test", opts)
   355  	p.client.Transport = newTestTransport()
   356  
   357  	dataChan := make(chan *metrics.EventMetrics, 100)
   358  
   359  	ctx, cancelF := context.WithCancel(context.Background())
   360  	p.updateTargetsAndStartProbes(ctx, dataChan)
   361  	if len(p.cancelFuncs) != 2 {
   362  		t.Errorf("len(p.cancelFunc)=%d, want=2", len(p.cancelFuncs))
   363  	}
   364  	ems, _ := testutils.MetricsFromChannel(dataChan, 100, time.Second)
   365  	compareNumberOfMetrics(t, ems, testTargets, true)
   366  
   367  	// Updates targets to just one target. This should cause one probe loop to
   368  	// exit. We should get only one data stream after that.
   369  	opts.Targets = targets.StaticTargets(testTargets[0])
   370  	p.updateTargetsAndStartProbes(ctx, dataChan)
   371  	if len(p.cancelFuncs) != 1 {
   372  		t.Errorf("len(p.cancelFunc)=%d, want=1", len(p.cancelFuncs))
   373  	}
   374  	ems, _ = testutils.MetricsFromChannel(dataChan, 100, time.Second)
   375  	compareNumberOfMetrics(t, ems, testTargets, false)
   376  
   377  	cancelF()
   378  	p.wait()
   379  }