github.com/netdata/go.d.plugin@v0.58.1/modules/httpcheck/httpcheck_test.go (about) 1 // SPDX-License-Identifier: GPL-3.0-or-later 2 3 package httpcheck 4 5 import ( 6 "net/http" 7 "net/http/httptest" 8 "testing" 9 "time" 10 11 "github.com/netdata/go.d.plugin/pkg/web" 12 13 "github.com/stretchr/testify/assert" 14 "github.com/stretchr/testify/require" 15 ) 16 17 func TestHTTPCheck_Init(t *testing.T) { 18 tests := map[string]struct { 19 wantFail bool 20 config Config 21 }{ 22 "success if url set": { 23 wantFail: false, 24 config: Config{ 25 HTTP: web.HTTP{ 26 Request: web.Request{URL: "http://127.0.0.1:38001"}, 27 }, 28 }, 29 }, 30 "fail with default": { 31 wantFail: true, 32 config: New().Config, 33 }, 34 "fail when URL not set": { 35 wantFail: true, 36 config: Config{ 37 HTTP: web.HTTP{ 38 Request: web.Request{URL: ""}, 39 }, 40 }, 41 }, 42 "fail if wrong response regex": { 43 wantFail: true, 44 config: Config{ 45 HTTP: web.HTTP{ 46 Request: web.Request{URL: "http://127.0.0.1:38001"}, 47 }, 48 ResponseMatch: "(?:qwe))", 49 }, 50 }, 51 } 52 53 for name, test := range tests { 54 t.Run(name, func(t *testing.T) { 55 httpCheck := New() 56 httpCheck.Config = test.config 57 58 if test.wantFail { 59 assert.False(t, httpCheck.Init()) 60 } else { 61 assert.True(t, httpCheck.Init()) 62 } 63 }) 64 } 65 } 66 67 func TestHTTPCheck_Charts(t *testing.T) { 68 tests := map[string]struct { 69 prepare func(t *testing.T) *HTTPCheck 70 wantCharts bool 71 }{ 72 "no charts if not inited": { 73 wantCharts: false, 74 prepare: func(t *testing.T) *HTTPCheck { 75 return New() 76 }, 77 }, 78 "charts if inited": { 79 wantCharts: true, 80 prepare: func(t *testing.T) *HTTPCheck { 81 httpCheck := New() 82 httpCheck.URL = "http://127.0.0.1:38001" 83 require.True(t, httpCheck.Init()) 84 85 return httpCheck 86 }, 87 }, 88 } 89 90 for name, test := range tests { 91 t.Run(name, func(t *testing.T) { 92 httpCheck := test.prepare(t) 93 94 if test.wantCharts { 95 assert.NotNil(t, httpCheck.Charts()) 96 } else { 97 assert.Nil(t, httpCheck.Charts()) 98 } 99 }) 100 } 101 } 102 103 func TestHTTPCheck_Cleanup(t *testing.T) { 104 httpCheck := New() 105 assert.NotPanics(t, httpCheck.Cleanup) 106 107 httpCheck.URL = "http://127.0.0.1:38001" 108 require.True(t, httpCheck.Init()) 109 assert.NotPanics(t, httpCheck.Cleanup) 110 } 111 112 func TestHTTPCheck_Check(t *testing.T) { 113 tests := map[string]struct { 114 prepare func() (httpCheck *HTTPCheck, cleanup func()) 115 wantFail bool 116 }{ 117 "success case": {wantFail: false, prepare: prepareSuccessCase}, 118 "timeout case": {wantFail: false, prepare: prepareTimeoutCase}, 119 "redirect success": {wantFail: false, prepare: prepareRedirectSuccessCase}, 120 "redirect fail": {wantFail: false, prepare: prepareRedirectFailCase}, 121 "bad status case": {wantFail: false, prepare: prepareBadStatusCase}, 122 "bad content case": {wantFail: false, prepare: prepareBadContentCase}, 123 "no connection case": {wantFail: false, prepare: prepareNoConnectionCase}, 124 "cookie auth case": {wantFail: false, prepare: prepareCookieAuthCase}, 125 } 126 127 for name, test := range tests { 128 t.Run(name, func(t *testing.T) { 129 httpCheck, cleanup := test.prepare() 130 defer cleanup() 131 132 require.True(t, httpCheck.Init()) 133 134 if test.wantFail { 135 assert.False(t, httpCheck.Check()) 136 } else { 137 assert.True(t, httpCheck.Check()) 138 } 139 }) 140 } 141 142 } 143 144 func TestHTTPCheck_Collect(t *testing.T) { 145 tests := map[string]struct { 146 prepare func() (httpCheck *HTTPCheck, cleanup func()) 147 update func(check *HTTPCheck) 148 wantMetrics map[string]int64 149 }{ 150 "success case": { 151 prepare: prepareSuccessCase, 152 wantMetrics: map[string]int64{ 153 "bad_content": 0, 154 "bad_header": 0, 155 "bad_status": 0, 156 "in_state": 2, 157 "length": 5, 158 "no_connection": 0, 159 "redirect": 0, 160 "success": 1, 161 "time": 0, 162 "timeout": 0, 163 }, 164 }, 165 "timeout case": { 166 prepare: prepareTimeoutCase, 167 wantMetrics: map[string]int64{ 168 "bad_content": 0, 169 "bad_header": 0, 170 "bad_status": 0, 171 "in_state": 2, 172 "length": 0, 173 "no_connection": 0, 174 "redirect": 0, 175 "success": 0, 176 "time": 0, 177 "timeout": 1, 178 }, 179 }, 180 "redirect success case": { 181 prepare: prepareRedirectSuccessCase, 182 wantMetrics: map[string]int64{ 183 "bad_content": 0, 184 "bad_header": 0, 185 "bad_status": 0, 186 "in_state": 2, 187 "length": 0, 188 "no_connection": 0, 189 "redirect": 0, 190 "success": 1, 191 "time": 0, 192 "timeout": 0, 193 }, 194 }, 195 "redirect fail case": { 196 prepare: prepareRedirectFailCase, 197 wantMetrics: map[string]int64{ 198 "bad_content": 0, 199 "bad_header": 0, 200 "bad_status": 0, 201 "in_state": 2, 202 "length": 0, 203 "no_connection": 0, 204 "redirect": 1, 205 "success": 0, 206 "time": 0, 207 "timeout": 0, 208 }, 209 }, 210 "bad status case": { 211 prepare: prepareBadStatusCase, 212 wantMetrics: map[string]int64{ 213 "bad_content": 0, 214 "bad_header": 0, 215 "bad_status": 1, 216 "in_state": 2, 217 "length": 0, 218 "no_connection": 0, 219 "redirect": 0, 220 "success": 0, 221 "time": 0, 222 "timeout": 0, 223 }, 224 }, 225 "bad content case": { 226 prepare: prepareBadContentCase, 227 wantMetrics: map[string]int64{ 228 "bad_content": 1, 229 "bad_header": 0, 230 "bad_status": 0, 231 "in_state": 2, 232 "length": 17, 233 "no_connection": 0, 234 "redirect": 0, 235 "success": 0, 236 "time": 0, 237 "timeout": 0, 238 }, 239 }, 240 "no connection case": { 241 prepare: prepareNoConnectionCase, 242 wantMetrics: map[string]int64{ 243 "bad_content": 0, 244 "bad_header": 0, 245 "bad_status": 0, 246 "in_state": 2, 247 "length": 0, 248 "no_connection": 1, 249 "redirect": 0, 250 "success": 0, 251 "time": 0, 252 "timeout": 0, 253 }, 254 }, 255 "header match include no value success case": { 256 prepare: prepareSuccessCase, 257 update: func(httpCheck *HTTPCheck) { 258 httpCheck.HeaderMatch = []HeaderMatchConfig{ 259 {Key: "header-key2"}, 260 } 261 }, 262 wantMetrics: map[string]int64{ 263 "bad_content": 0, 264 "bad_header": 0, 265 "bad_status": 0, 266 "in_state": 2, 267 "length": 5, 268 "no_connection": 0, 269 "redirect": 0, 270 "success": 1, 271 "time": 0, 272 "timeout": 0, 273 }, 274 }, 275 "header match include with value success case": { 276 prepare: prepareSuccessCase, 277 update: func(httpCheck *HTTPCheck) { 278 httpCheck.HeaderMatch = []HeaderMatchConfig{ 279 {Key: "header-key2", Value: "= header-value"}, 280 } 281 }, 282 wantMetrics: map[string]int64{ 283 "bad_content": 0, 284 "bad_header": 0, 285 "bad_status": 0, 286 "in_state": 2, 287 "length": 5, 288 "no_connection": 0, 289 "redirect": 0, 290 "success": 1, 291 "time": 0, 292 "timeout": 0, 293 }, 294 }, 295 "header match include no value bad headers case": { 296 prepare: prepareSuccessCase, 297 update: func(httpCheck *HTTPCheck) { 298 httpCheck.HeaderMatch = []HeaderMatchConfig{ 299 {Key: "header-key99"}, 300 } 301 }, 302 wantMetrics: map[string]int64{ 303 "bad_content": 0, 304 "bad_header": 1, 305 "bad_status": 0, 306 "in_state": 2, 307 "length": 5, 308 "no_connection": 0, 309 "redirect": 0, 310 "success": 0, 311 "time": 0, 312 "timeout": 0, 313 }, 314 }, 315 "header match include with value bad headers case": { 316 prepare: prepareSuccessCase, 317 update: func(httpCheck *HTTPCheck) { 318 httpCheck.HeaderMatch = []HeaderMatchConfig{ 319 {Key: "header-key2", Value: "= header-value99"}, 320 } 321 }, 322 wantMetrics: map[string]int64{ 323 "bad_content": 0, 324 "bad_header": 1, 325 "bad_status": 0, 326 "in_state": 2, 327 "length": 5, 328 "no_connection": 0, 329 "redirect": 0, 330 "success": 0, 331 "time": 0, 332 "timeout": 0, 333 }, 334 }, 335 "header match exclude no value success case": { 336 prepare: prepareSuccessCase, 337 update: func(httpCheck *HTTPCheck) { 338 httpCheck.HeaderMatch = []HeaderMatchConfig{ 339 {Exclude: true, Key: "header-key99"}, 340 } 341 }, 342 wantMetrics: map[string]int64{ 343 "bad_content": 0, 344 "bad_header": 0, 345 "bad_status": 0, 346 "in_state": 2, 347 "length": 5, 348 "no_connection": 0, 349 "redirect": 0, 350 "success": 1, 351 "time": 0, 352 "timeout": 0, 353 }, 354 }, 355 "header match exclude with value success case": { 356 prepare: prepareSuccessCase, 357 update: func(httpCheck *HTTPCheck) { 358 httpCheck.HeaderMatch = []HeaderMatchConfig{ 359 {Exclude: true, Key: "header-key2", Value: "= header-value99"}, 360 } 361 }, 362 wantMetrics: map[string]int64{ 363 "bad_content": 0, 364 "bad_header": 0, 365 "bad_status": 0, 366 "in_state": 2, 367 "length": 5, 368 "no_connection": 0, 369 "redirect": 0, 370 "success": 1, 371 "time": 0, 372 "timeout": 0, 373 }, 374 }, 375 "header match exclude no value bad headers case": { 376 prepare: prepareSuccessCase, 377 update: func(httpCheck *HTTPCheck) { 378 httpCheck.HeaderMatch = []HeaderMatchConfig{ 379 {Exclude: true, Key: "header-key2"}, 380 } 381 }, 382 wantMetrics: map[string]int64{ 383 "bad_content": 0, 384 "bad_header": 1, 385 "bad_status": 0, 386 "in_state": 2, 387 "length": 5, 388 "no_connection": 0, 389 "redirect": 0, 390 "success": 0, 391 "time": 0, 392 "timeout": 0, 393 }, 394 }, 395 "header match exclude with value bad headers case": { 396 prepare: prepareSuccessCase, 397 update: func(httpCheck *HTTPCheck) { 398 httpCheck.HeaderMatch = []HeaderMatchConfig{ 399 {Exclude: true, Key: "header-key2", Value: "= header-value"}, 400 } 401 }, 402 wantMetrics: map[string]int64{ 403 "bad_content": 0, 404 "bad_header": 1, 405 "bad_status": 0, 406 "in_state": 2, 407 "length": 5, 408 "no_connection": 0, 409 "redirect": 0, 410 "success": 0, 411 "time": 0, 412 "timeout": 0, 413 }, 414 }, 415 "cookie auth case": { 416 prepare: prepareCookieAuthCase, 417 wantMetrics: map[string]int64{ 418 "bad_content": 0, 419 "bad_header": 0, 420 "bad_status": 0, 421 "in_state": 2, 422 "length": 0, 423 "no_connection": 0, 424 "redirect": 0, 425 "success": 1, 426 "time": 0, 427 "timeout": 0, 428 }, 429 }, 430 } 431 432 for name, test := range tests { 433 t.Run(name, func(t *testing.T) { 434 httpCheck, cleanup := test.prepare() 435 defer cleanup() 436 437 if test.update != nil { 438 test.update(httpCheck) 439 } 440 441 require.True(t, httpCheck.Init()) 442 443 var mx map[string]int64 444 445 for i := 0; i < 2; i++ { 446 mx = httpCheck.Collect() 447 time.Sleep(time.Duration(httpCheck.UpdateEvery) * time.Second) 448 } 449 450 copyResponseTime(test.wantMetrics, mx) 451 452 require.Equal(t, test.wantMetrics, mx) 453 }) 454 } 455 } 456 457 func prepareSuccessCase() (*HTTPCheck, func()) { 458 httpCheck := New() 459 httpCheck.UpdateEvery = 1 460 httpCheck.ResponseMatch = "match" 461 462 srv := httptest.NewServer(http.HandlerFunc( 463 func(w http.ResponseWriter, r *http.Request) { 464 w.Header().Set("header-key1", "header-value") 465 w.Header().Set("header-key2", "header-value") 466 w.WriteHeader(http.StatusOK) 467 _, _ = w.Write([]byte("match")) 468 })) 469 470 httpCheck.URL = srv.URL 471 472 return httpCheck, srv.Close 473 } 474 475 func prepareTimeoutCase() (*HTTPCheck, func()) { 476 httpCheck := New() 477 httpCheck.UpdateEvery = 1 478 httpCheck.Timeout.Duration = time.Millisecond * 100 479 480 srv := httptest.NewServer(http.HandlerFunc( 481 func(w http.ResponseWriter, r *http.Request) { 482 time.Sleep(httpCheck.Timeout.Duration + time.Millisecond*100) 483 })) 484 485 httpCheck.URL = srv.URL 486 487 return httpCheck, srv.Close 488 } 489 490 func prepareRedirectSuccessCase() (*HTTPCheck, func()) { 491 httpCheck := New() 492 httpCheck.UpdateEvery = 1 493 httpCheck.NotFollowRedirect = true 494 httpCheck.AcceptedStatuses = []int{301} 495 496 srv := httptest.NewServer(http.HandlerFunc( 497 func(w http.ResponseWriter, r *http.Request) { 498 http.Redirect(w, r, "https://example.com", http.StatusMovedPermanently) 499 })) 500 501 httpCheck.URL = srv.URL 502 503 return httpCheck, srv.Close 504 } 505 506 func prepareRedirectFailCase() (*HTTPCheck, func()) { 507 httpCheck := New() 508 httpCheck.UpdateEvery = 1 509 httpCheck.NotFollowRedirect = true 510 511 srv := httptest.NewServer(http.HandlerFunc( 512 func(w http.ResponseWriter, r *http.Request) { 513 http.Redirect(w, r, "https://example.com", http.StatusMovedPermanently) 514 })) 515 516 httpCheck.URL = srv.URL 517 518 return httpCheck, srv.Close 519 } 520 521 func prepareBadStatusCase() (*HTTPCheck, func()) { 522 httpCheck := New() 523 httpCheck.UpdateEvery = 1 524 525 srv := httptest.NewServer(http.HandlerFunc( 526 func(w http.ResponseWriter, r *http.Request) { 527 w.WriteHeader(http.StatusBadGateway) 528 })) 529 530 httpCheck.URL = srv.URL 531 532 return httpCheck, srv.Close 533 } 534 535 func prepareBadContentCase() (*HTTPCheck, func()) { 536 httpCheck := New() 537 httpCheck.UpdateEvery = 1 538 httpCheck.ResponseMatch = "no match" 539 540 srv := httptest.NewServer(http.HandlerFunc( 541 func(w http.ResponseWriter, r *http.Request) { 542 w.WriteHeader(http.StatusOK) 543 _, _ = w.Write([]byte("hello and goodbye")) 544 })) 545 546 httpCheck.URL = srv.URL 547 548 return httpCheck, srv.Close 549 } 550 551 func prepareNoConnectionCase() (*HTTPCheck, func()) { 552 httpCheck := New() 553 httpCheck.UpdateEvery = 1 554 httpCheck.URL = "http://127.0.0.1:38001" 555 556 return httpCheck, func() {} 557 } 558 559 func prepareCookieAuthCase() (*HTTPCheck, func()) { 560 httpCheck := New() 561 httpCheck.UpdateEvery = 1 562 httpCheck.CookieFile = "testdata/cookie.txt" 563 564 srv := httptest.NewServer(http.HandlerFunc( 565 func(w http.ResponseWriter, r *http.Request) { 566 if _, err := r.Cookie("JSESSIONID"); err != nil { 567 w.WriteHeader(http.StatusUnauthorized) 568 } else { 569 w.WriteHeader(http.StatusOK) 570 } 571 })) 572 573 httpCheck.URL = srv.URL 574 575 return httpCheck, srv.Close 576 } 577 578 func copyResponseTime(dst, src map[string]int64) { 579 if v, ok := src["time"]; ok { 580 if _, ok := dst["time"]; ok { 581 dst["time"] = v 582 } 583 } 584 }