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  }