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