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 }