package http_response import ( "errors" "fmt" "io" "net" "net/http" "net/http/httptest" "net/url" "testing" "time" "github.com/stretchr/testify/require" "github.com/influxdata/telegraf" "github.com/influxdata/telegraf/config" "github.com/influxdata/telegraf/internal" "github.com/influxdata/telegraf/plugins/common/tls" "github.com/influxdata/telegraf/testutil" ) // Receives a list with fields that are expected to be absent func checkAbsentFields(t *testing.T, fields []string, acc *testutil.Accumulator) { for _, field := range fields { ok := acc.HasField("http_response", field) require.False(t, ok) } } // Receives a list with tags that are expected to be absent func checkAbsentTags(t *testing.T, tags []string, acc *testutil.Accumulator) { for _, tag := range tags { ok := acc.HasTag("http_response", tag) require.False(t, ok) } } // Receives a dictionary and with expected fields and their values. If a value is nil, it will only check // that the field exists, but not its contents func checkFields(t *testing.T, fields map[string]interface{}, acc *testutil.Accumulator) { t.Helper() for key, field := range fields { switch v := field.(type) { case int: value, ok := acc.IntField("http_response", key) require.True(t, ok) require.Equal(t, field, value) case float64: value, ok := acc.FloatField("http_response", key) require.True(t, ok) require.InDelta(t, field, value, testutil.DefaultDelta) case string: value, ok := acc.StringField("http_response", key) require.True(t, ok) require.Equal(t, field, value) case nil: ok := acc.HasField("http_response", key) require.True(t, ok) default: t.Log("Unsupported type for field: ", v) t.Fail() } } } // Receives a dictionary and with expected tags and their values. If a value is nil, it will only check // that the tag exists, but not its contents func checkTags(t *testing.T, tags map[string]interface{}, acc *testutil.Accumulator) { for key, tag := range tags { switch v := tag.(type) { case string: ok := acc.HasTag("http_response", key) require.True(t, ok) require.Equal(t, tag, acc.TagValue("http_response", key)) case nil: ok := acc.HasTag("http_response", key) require.True(t, ok) default: t.Log("Unsupported type for tag: ", v) t.Fail() } } } func setUpTestMux() http.Handler { mux := http.NewServeMux() // Ignore all returned errors below as the tests will fail anyway mux.HandleFunc("/redirect", func(w http.ResponseWriter, req *http.Request) { http.Redirect(w, req, "/good", http.StatusMovedPermanently) }) mux.HandleFunc("/good", func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Server", "MyTestServer") w.Header().Set("Content-Type", "application/json; charset=utf-8") fmt.Fprintf(w, "hit the good page!") }) mux.HandleFunc("/form", func(w http.ResponseWriter, req *http.Request) { body, err := io.ReadAll(req.Body) defer req.Body.Close() if err != nil { http.Error(w, "couldn't read request body", http.StatusBadRequest) return } if string(body) != "list=foobar&list=fizbuzz&test=42" { fmt.Println(string(body)) w.WriteHeader(http.StatusBadRequest) } else { w.WriteHeader(http.StatusOK) } }) mux.HandleFunc("/invalidUTF8", func(w http.ResponseWriter, _ *http.Request) { w.Write([]byte{0xff, 0xfe, 0xfd}) //nolint:errcheck // ignore the returned error as the test will fail anyway }) mux.HandleFunc("/noheader", func(w http.ResponseWriter, _ *http.Request) { fmt.Fprintf(w, "hit the good page!") }) mux.HandleFunc("/jsonresponse", func(w http.ResponseWriter, _ *http.Request) { fmt.Fprintf(w, "\"service_status\": \"up\", \"healthy\" : \"true\"") }) mux.HandleFunc("/badredirect", func(w http.ResponseWriter, req *http.Request) { http.Redirect(w, req, "/badredirect", http.StatusMovedPermanently) }) mux.HandleFunc("/mustbepostmethod", func(w http.ResponseWriter, req *http.Request) { if req.Method != "POST" { http.Error(w, "method wasn't post", http.StatusMethodNotAllowed) return } fmt.Fprintf(w, "used post correctly!") }) mux.HandleFunc("/musthaveabody", func(w http.ResponseWriter, req *http.Request) { body, err := io.ReadAll(req.Body) defer req.Body.Close() if err != nil { http.Error(w, "couldn't read request body", http.StatusBadRequest) return } if len(body) == 0 { http.Error(w, "body was empty", http.StatusBadRequest) return } fmt.Fprintf(w, "sent a body!") }) mux.HandleFunc("/twosecondnap", func(http.ResponseWriter, *http.Request) { time.Sleep(time.Second * 2) }) mux.HandleFunc("/nocontent", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNoContent) }) return mux } func checkOutput(t *testing.T, acc *testutil.Accumulator, presentFields, presentTags map[string]interface{}, absentFields, absentTags []string) { t.Helper() if presentFields != nil { checkFields(t, presentFields, acc) } if presentTags != nil { checkTags(t, presentTags, acc) } if absentFields != nil { checkAbsentFields(t, absentFields, acc) } if absentTags != nil { checkAbsentTags(t, absentTags, acc) } } func TestHeaders(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Host != "Hello" { w.WriteHeader(http.StatusInternalServerError) t.Errorf("Not equal, expected: %q, actual: %q", "Hello", r.Host) return } if cHeader := r.Header.Get("Content-Type"); cHeader != "application/json" { w.WriteHeader(http.StatusInternalServerError) t.Errorf("Not equal, expected: %q, actual: %q", "application/json", cHeader) return } if uaHeader := r.Header.Get("User-Agent"); uaHeader != internal.ProductToken() { w.WriteHeader(http.StatusInternalServerError) t.Errorf("Not equal, expected: %q, actual: %q", internal.ProductToken(), uaHeader) return } w.WriteHeader(http.StatusOK) })) defer ts.Close() h := &HTTPResponse{ Log: testutil.Logger{}, URLs: []string{ts.URL}, Method: "GET", ResponseTimeout: config.Duration(time.Second * 2), Headers: map[string]string{ "Content-Type": "application/json", "Host": "Hello", }, } var acc testutil.Accumulator require.NoError(t, h.Init()) require.NoError(t, h.Gather(&acc)) expectedFields := map[string]interface{}{ "http_response_code": http.StatusOK, "result_type": "success", "result_code": 0, "response_time": nil, "content_length": nil, } expectedTags := map[string]interface{}{ "server": nil, "method": "GET", "status_code": "200", "result": "success", } absentFields := []string{"response_string_match"} checkOutput(t, &acc, expectedFields, expectedTags, absentFields, nil) } func TestFields(t *testing.T) { mux := setUpTestMux() ts := httptest.NewServer(mux) defer ts.Close() h := &HTTPResponse{ Log: testutil.Logger{}, URLs: []string{ts.URL + "/good"}, Body: "{ 'test': 'data'}", Method: "GET", ResponseTimeout: config.Duration(time.Second * 20), Headers: map[string]string{ "Content-Type": "application/json", }, FollowRedirects: true, } var acc testutil.Accumulator require.NoError(t, h.Init()) require.NoError(t, h.Gather(&acc)) expectedFields := map[string]interface{}{ "http_response_code": http.StatusOK, "result_type": "success", "result_code": 0, "response_time": nil, "content_length": nil, } expectedTags := map[string]interface{}{ "server": nil, "method": "GET", "status_code": "200", "result": "success", } absentFields := []string{"response_string_match"} checkOutput(t, &acc, expectedFields, expectedTags, absentFields, nil) } func TestResponseBodyField(t *testing.T) { mux := setUpTestMux() ts := httptest.NewServer(mux) defer ts.Close() h := &HTTPResponse{ Log: testutil.Logger{}, URLs: []string{ts.URL + "/good"}, Body: "{ 'test': 'data'}", Method: "GET", ResponseTimeout: config.Duration(time.Second * 20), Headers: map[string]string{ "Content-Type": "application/json", }, ResponseBodyField: "my_body_field", FollowRedirects: true, } var acc testutil.Accumulator require.NoError(t, h.Init()) require.NoError(t, h.Gather(&acc)) expectedFields := map[string]interface{}{ "http_response_code": http.StatusOK, "result_type": "success", "result_code": 0, "response_time": nil, "content_length": nil, "my_body_field": "hit the good page!", } expectedTags := map[string]interface{}{ "server": nil, "method": "GET", "status_code": "200", "result": "success", } absentFields := []string{"response_string_match"} checkOutput(t, &acc, expectedFields, expectedTags, absentFields, nil) // Invalid UTF-8 String h = &HTTPResponse{ Log: testutil.Logger{}, URLs: []string{ts.URL + "/invalidUTF8"}, Body: "{ 'test': 'data'}", Method: "GET", ResponseTimeout: config.Duration(time.Second * 20), Headers: map[string]string{ "Content-Type": "application/json", }, ResponseBodyField: "my_body_field", FollowRedirects: true, } acc = testutil.Accumulator{} require.NoError(t, h.Init()) require.NoError(t, h.Gather(&acc)) expectedFields = map[string]interface{}{ "result_type": "body_read_error", "result_code": 2, } expectedTags = map[string]interface{}{ "server": nil, "method": "GET", "result": "body_read_error", } checkOutput(t, &acc, expectedFields, expectedTags, nil, nil) } func TestResponseBodyFormField(t *testing.T) { mux := setUpTestMux() ts := httptest.NewServer(mux) defer ts.Close() h := &HTTPResponse{ Log: testutil.Logger{}, URLs: []string{ts.URL + "/form"}, BodyForm: map[string][]string{ "test": {"42"}, "list": {"foobar", "fizbuzz"}, }, Method: "POST", Headers: map[string]string{ "Content-Type": "application/x-www-form-urlencoded", }, ResponseTimeout: config.Duration(time.Second * 20), ResponseBodyField: "my_body_field", } var acc testutil.Accumulator require.NoError(t, h.Init()) require.NoError(t, h.Gather(&acc)) expectedFields := map[string]interface{}{ "http_response_code": http.StatusOK, "result_type": "success", "result_code": 0, "response_time": nil, "content_length": nil, "my_body_field": "", } expectedTags := map[string]interface{}{ "server": nil, "method": "POST", "status_code": "200", "result": "success", } checkOutput(t, &acc, expectedFields, expectedTags, nil, nil) } func TestResponseBodyMaxSize(t *testing.T) { mux := setUpTestMux() ts := httptest.NewServer(mux) defer ts.Close() h := &HTTPResponse{ Log: testutil.Logger{}, URLs: []string{ts.URL + "/good"}, Body: "{ 'test': 'data'}", Method: "GET", ResponseTimeout: config.Duration(time.Second * 20), Headers: map[string]string{ "Content-Type": "application/json", }, ResponseBodyMaxSize: config.Size(5), FollowRedirects: true, } var acc testutil.Accumulator require.NoError(t, h.Init()) require.NoError(t, h.Gather(&acc)) expectedFields := map[string]interface{}{ "result_type": "body_read_error", "result_code": 2, } expectedTags := map[string]interface{}{ "server": nil, "method": "GET", "result": "body_read_error", } checkOutput(t, &acc, expectedFields, expectedTags, nil, nil) } func TestHTTPHeaderTags(t *testing.T) { mux := setUpTestMux() ts := httptest.NewServer(mux) defer ts.Close() h := &HTTPResponse{ Log: testutil.Logger{}, URLs: []string{ts.URL + "/good"}, Body: "{ 'test': 'data'}", Method: "GET", ResponseTimeout: config.Duration(time.Second * 20), HTTPHeaderTags: map[string]string{"Server": "my_server", "Content-Type": "content_type"}, Headers: map[string]string{ "Content-Type": "application/json", }, FollowRedirects: true, } var acc testutil.Accumulator require.NoError(t, h.Init()) require.NoError(t, h.Gather(&acc)) expectedFields := map[string]interface{}{ "http_response_code": http.StatusOK, "result_type": "success", "result_code": 0, "response_time": nil, "content_length": nil, } expectedTags := map[string]interface{}{ "server": nil, "method": "GET", "status_code": "200", "result": "success", "my_server": "MyTestServer", "content_type": "application/json; charset=utf-8", } absentFields := []string{"response_string_match"} checkOutput(t, &acc, expectedFields, expectedTags, absentFields, nil) h = &HTTPResponse{ Log: testutil.Logger{}, URLs: []string{ts.URL + "/noheader"}, Body: "{ 'test': 'data'}", Method: "GET", ResponseTimeout: config.Duration(time.Second * 20), HTTPHeaderTags: map[string]string{"Server": "my_server", "Content-Type": "content_type"}, Headers: map[string]string{ "Content-Type": "application/json", }, FollowRedirects: true, } acc = testutil.Accumulator{} require.NoError(t, h.Init()) require.NoError(t, h.Gather(&acc)) expectedTags = map[string]interface{}{ "server": nil, "method": "GET", "status_code": "200", "result": "success", } checkOutput(t, &acc, expectedFields, expectedTags, absentFields, nil) // Connection failed h = &HTTPResponse{ Log: testutil.Logger{}, URLs: []string{"https:/nonexistent.nonexistent"}, // Any non-routable IP works here Body: "", Method: "GET", ResponseTimeout: config.Duration(time.Second * 5), HTTPHeaderTags: map[string]string{"Server": "my_server", "Content-Type": "content_type"}, FollowRedirects: false, } acc = testutil.Accumulator{} require.NoError(t, h.Init()) require.NoError(t, h.Gather(&acc)) expectedFields = map[string]interface{}{ "result_type": "connection_failed", "result_code": 3, } expectedTags = map[string]interface{}{ "server": nil, "method": "GET", "result": "connection_failed", } absentFields = []string{"http_response_code", "response_time", "content_length", "response_string_match"} checkOutput(t, &acc, expectedFields, expectedTags, absentFields, nil) } func findInterface() (net.Interface, error) { potential, err := net.Interfaces() if err != nil { return net.Interface{}, err } for _, i := range potential { // we are only interest in loopback interfaces which are up if (i.Flags&net.FlagUp == 0) || (i.Flags&net.FlagLoopback == 0) { continue } if addrs, err := i.Addrs(); err == nil && len(addrs) > 0 { return i, nil } } return net.Interface{}, errors.New("cannot find suitable loopback interface") } func TestInterface(t *testing.T) { var ( mux = setUpTestMux() ts = httptest.NewServer(mux) ) defer ts.Close() intf, err := findInterface() require.NoError(t, err) h := &HTTPResponse{ Log: testutil.Logger{}, URLs: []string{ts.URL + "/good"}, Body: "{ 'test': 'data'}", Method: "GET", ResponseTimeout: config.Duration(time.Second * 20), Headers: map[string]string{ "Content-Type": "application/json", }, FollowRedirects: true, Interface: intf.Name, } var acc testutil.Accumulator require.NoError(t, h.Init()) require.NoError(t, h.Gather(&acc)) expectedFields := map[string]interface{}{ "http_response_code": http.StatusOK, "result_type": "success", "result_code": 0, "response_time": nil, "content_length": nil, } expectedTags := map[string]interface{}{ "server": nil, "method": "GET", "status_code": "200", "result": "success", } absentFields := []string{"response_string_match"} checkOutput(t, &acc, expectedFields, expectedTags, absentFields, nil) } func TestRedirects(t *testing.T) { mux := setUpTestMux() ts := httptest.NewServer(mux) defer ts.Close() h := &HTTPResponse{ Log: testutil.Logger{}, URLs: []string{ts.URL + "/redirect"}, Body: "{ 'test': 'data'}", Method: "GET", ResponseTimeout: config.Duration(time.Second * 20), Headers: map[string]string{ "Content-Type": "application/json", }, FollowRedirects: true, } var acc testutil.Accumulator require.NoError(t, h.Init()) require.NoError(t, h.Gather(&acc)) expectedFields := map[string]interface{}{ "http_response_code": http.StatusOK, "result_type": "success", "result_code": 0, "response_time": nil, "content_length": nil, } expectedTags := map[string]interface{}{ "server": nil, "method": "GET", "status_code": "200", "result": "success", } absentFields := []string{"response_string_match"} checkOutput(t, &acc, expectedFields, expectedTags, absentFields, nil) h = &HTTPResponse{ Log: testutil.Logger{}, URLs: []string{ts.URL + "/badredirect"}, Body: "{ 'test': 'data'}", Method: "GET", ResponseTimeout: config.Duration(time.Second * 20), Headers: map[string]string{ "Content-Type": "application/json", }, FollowRedirects: true, } acc = testutil.Accumulator{} require.NoError(t, h.Init()) require.NoError(t, h.Gather(&acc)) expectedFields = map[string]interface{}{ "result_type": "connection_failed", "result_code": 3, } expectedTags = map[string]interface{}{ "server": nil, "method": "GET", "result": "connection_failed", } absentFields = []string{"http_response_code", "response_time", "response_string_match"} absentTags := []string{"status_code"} checkOutput(t, &acc, expectedFields, expectedTags, nil, nil) expectedFields = map[string]interface{}{"result_type": "connection_failed"} checkOutput(t, &acc, expectedFields, expectedTags, absentFields, absentTags) } func TestMethod(t *testing.T) { mux := setUpTestMux() ts := httptest.NewServer(mux) defer ts.Close() h := &HTTPResponse{ Log: testutil.Logger{}, URLs: []string{ts.URL + "/mustbepostmethod"}, Body: "{ 'test': 'data'}", Method: "POST", ResponseTimeout: config.Duration(time.Second * 20), Headers: map[string]string{ "Content-Type": "application/json", }, FollowRedirects: true, } var acc testutil.Accumulator require.NoError(t, h.Init()) require.NoError(t, h.Gather(&acc)) expectedFields := map[string]interface{}{ "http_response_code": http.StatusOK, "result_type": "success", "result_code": 0, "response_time": nil, "content_length": nil, } expectedTags := map[string]interface{}{ "server": nil, "method": "POST", "status_code": "200", "result": "success", } absentFields := []string{"response_string_match"} checkOutput(t, &acc, expectedFields, expectedTags, absentFields, nil) h = &HTTPResponse{ Log: testutil.Logger{}, URLs: []string{ts.URL + "/mustbepostmethod"}, Body: "{ 'test': 'data'}", Method: "GET", ResponseTimeout: config.Duration(time.Second * 20), Headers: map[string]string{ "Content-Type": "application/json", }, FollowRedirects: true, } acc = testutil.Accumulator{} require.NoError(t, h.Init()) require.NoError(t, h.Gather(&acc)) expectedFields = map[string]interface{}{ "http_response_code": http.StatusMethodNotAllowed, "result_type": "success", "result_code": 0, "response_time": nil, "content_length": nil, } expectedTags = map[string]interface{}{ "server": nil, "method": "GET", "status_code": "405", "result": "success", } absentFields = []string{"response_string_match"} checkOutput(t, &acc, expectedFields, expectedTags, absentFields, nil) // check that lowercase methods work correctly h = &HTTPResponse{ Log: testutil.Logger{}, URLs: []string{ts.URL + "/mustbepostmethod"}, Body: "{ 'test': 'data'}", Method: "head", ResponseTimeout: config.Duration(time.Second * 20), Headers: map[string]string{ "Content-Type": "application/json", }, FollowRedirects: true, } acc = testutil.Accumulator{} require.NoError(t, h.Init()) require.NoError(t, h.Gather(&acc)) expectedFields = map[string]interface{}{ "http_response_code": http.StatusMethodNotAllowed, "result_type": "success", "result_code": 0, "response_time": nil, "content_length": nil, } expectedTags = map[string]interface{}{ "server": nil, "method": "head", "status_code": "405", "result": "success", } absentFields = []string{"response_string_match"} checkOutput(t, &acc, expectedFields, expectedTags, absentFields, nil) } func TestBody(t *testing.T) { mux := setUpTestMux() ts := httptest.NewServer(mux) defer ts.Close() h := &HTTPResponse{ Log: testutil.Logger{}, URLs: []string{ts.URL + "/musthaveabody"}, Body: "{ 'test': 'data'}", Method: "GET", ResponseTimeout: config.Duration(time.Second * 20), Headers: map[string]string{ "Content-Type": "application/json", }, FollowRedirects: true, } var acc testutil.Accumulator require.NoError(t, h.Init()) require.NoError(t, h.Gather(&acc)) expectedFields := map[string]interface{}{ "http_response_code": http.StatusOK, "result_type": "success", "result_code": 0, "response_time": nil, "content_length": nil, } expectedTags := map[string]interface{}{ "server": nil, "method": "GET", "status_code": "200", "result": "success", } absentFields := []string{"response_string_match"} checkOutput(t, &acc, expectedFields, expectedTags, absentFields, nil) h = &HTTPResponse{ Log: testutil.Logger{}, URLs: []string{ts.URL + "/musthaveabody"}, Method: "GET", ResponseTimeout: config.Duration(time.Second * 20), Headers: map[string]string{ "Content-Type": "application/json", }, FollowRedirects: true, } acc = testutil.Accumulator{} require.NoError(t, h.Init()) require.NoError(t, h.Gather(&acc)) expectedFields = map[string]interface{}{ "http_response_code": http.StatusBadRequest, "result_type": "success", "result_code": 0, } expectedTags = map[string]interface{}{ "server": nil, "method": "GET", "status_code": "400", "result": "success", } absentFields = []string{"response_string_match"} checkOutput(t, &acc, expectedFields, expectedTags, absentFields, nil) } func TestStringMatch(t *testing.T) { mux := setUpTestMux() ts := httptest.NewServer(mux) defer ts.Close() h := &HTTPResponse{ Log: testutil.Logger{}, URLs: []string{ts.URL + "/good"}, Body: "{ 'test': 'data'}", Method: "GET", ResponseStringMatch: "hit the good page", ResponseTimeout: config.Duration(time.Second * 20), Headers: map[string]string{ "Content-Type": "application/json", }, FollowRedirects: true, } var acc testutil.Accumulator require.NoError(t, h.Init()) require.NoError(t, h.Gather(&acc)) expectedFields := map[string]interface{}{ "http_response_code": http.StatusOK, "response_string_match": 1, "result_type": "success", "result_code": 0, "response_time": nil, "content_length": nil, } expectedTags := map[string]interface{}{ "server": nil, "method": "GET", "status_code": "200", "result": "success", } checkOutput(t, &acc, expectedFields, expectedTags, nil, nil) } func TestStringMatchJson(t *testing.T) { mux := setUpTestMux() ts := httptest.NewServer(mux) defer ts.Close() h := &HTTPResponse{ Log: testutil.Logger{}, URLs: []string{ts.URL + "/jsonresponse"}, Body: "{ 'test': 'data'}", Method: "GET", ResponseStringMatch: "\"service_status\": \"up\"", ResponseTimeout: config.Duration(time.Second * 20), Headers: map[string]string{ "Content-Type": "application/json", }, FollowRedirects: true, } var acc testutil.Accumulator require.NoError(t, h.Init()) require.NoError(t, h.Gather(&acc)) expectedFields := map[string]interface{}{ "http_response_code": http.StatusOK, "response_string_match": 1, "result_type": "success", "result_code": 0, "response_time": nil, "content_length": nil, } expectedTags := map[string]interface{}{ "server": nil, "method": "GET", "status_code": "200", "result": "success", } checkOutput(t, &acc, expectedFields, expectedTags, nil, nil) } func TestStringMatchFail(t *testing.T) { mux := setUpTestMux() ts := httptest.NewServer(mux) defer ts.Close() h := &HTTPResponse{ Log: testutil.Logger{}, URLs: []string{ts.URL + "/good"}, Body: "{ 'test': 'data'}", Method: "GET", ResponseStringMatch: "hit the bad page", ResponseTimeout: config.Duration(time.Second * 20), Headers: map[string]string{ "Content-Type": "application/json", }, FollowRedirects: true, } var acc testutil.Accumulator require.NoError(t, h.Init()) require.NoError(t, h.Gather(&acc)) expectedFields := map[string]interface{}{ "http_response_code": http.StatusOK, "response_string_match": 0, "result_type": "response_string_mismatch", "result_code": 1, "response_time": nil, "content_length": nil, } expectedTags := map[string]interface{}{ "server": nil, "method": "GET", "status_code": "200", "result": "response_string_mismatch", } checkOutput(t, &acc, expectedFields, expectedTags, nil, nil) } func TestTimeout(t *testing.T) { if testing.Short() { t.Skip("Skipping test with sleep in short mode.") } mux := setUpTestMux() ts := httptest.NewServer(mux) defer ts.Close() h := &HTTPResponse{ Log: testutil.Logger{}, URLs: []string{ts.URL + "/twosecondnap"}, Body: "{ 'test': 'data'}", Method: "GET", ResponseTimeout: config.Duration(time.Second), Headers: map[string]string{ "Content-Type": "application/json", }, FollowRedirects: true, } var acc testutil.Accumulator require.NoError(t, h.Init()) require.NoError(t, h.Gather(&acc)) expectedFields := map[string]interface{}{ "result_type": "timeout", "result_code": 4, } expectedTags := map[string]interface{}{ "server": nil, "method": "GET", "result": "timeout", } absentFields := []string{"http_response_code", "response_time", "content_length", "response_string_match"} absentTags := []string{"status_code"} checkOutput(t, &acc, expectedFields, expectedTags, absentFields, absentTags) } func TestBadRegex(t *testing.T) { mux := setUpTestMux() ts := httptest.NewServer(mux) defer ts.Close() h := &HTTPResponse{ Log: testutil.Logger{}, URLs: []string{ts.URL + "/good"}, Body: "{ 'test': 'data'}", Method: "GET", ResponseStringMatch: "bad regex:[[", ResponseTimeout: config.Duration(time.Second * 20), Headers: map[string]string{ "Content-Type": "application/json", }, FollowRedirects: true, } require.ErrorContains(t, h.Init(), "failed to compile regular expression") } type fakeClient struct { statusCode int err error } func (f *fakeClient) Do(_ *http.Request) (*http.Response, error) { return &http.Response{StatusCode: f.statusCode}, f.err } func TestNetworkErrors(t *testing.T) { cl := client{ httpClient: &fakeClient{err: &url.Error{Err: &net.OpError{Err: &net.DNSError{Err: "DNS error"}}}}, address: "", } // DNS error h := &HTTPResponse{ Log: testutil.Logger{}, URLs: []string{"https://nonexistent.nonexistent"}, // Any non-resolvable URL works here Body: "", Method: "GET", ResponseTimeout: config.Duration(time.Second * 20), FollowRedirects: false, clients: []client{cl}, } var acc testutil.Accumulator require.NoError(t, h.Init()) require.NoError(t, h.Gather(&acc)) expectedFields := map[string]interface{}{ "result_type": "dns_error", "result_code": 5, } expectedTags := map[string]interface{}{ "server": nil, "method": "GET", "result": "dns_error", } absentFields := []string{"http_response_code", "response_time", "content_length", "response_string_match"} absentTags := []string{"status_code"} checkOutput(t, &acc, expectedFields, expectedTags, absentFields, absentTags) // Connection failed h = &HTTPResponse{ Log: testutil.Logger{}, URLs: []string{"https:/nonexistent.nonexistent"}, // Any non-routable IP works here Body: "", Method: "GET", ResponseTimeout: config.Duration(time.Second * 5), FollowRedirects: false, } acc = testutil.Accumulator{} require.NoError(t, h.Init()) require.NoError(t, h.Gather(&acc)) expectedFields = map[string]interface{}{ "result_type": "connection_failed", "result_code": 3, } expectedTags = map[string]interface{}{ "server": nil, "method": "GET", "result": "connection_failed", } absentFields = []string{"http_response_code", "response_time", "content_length", "response_string_match"} absentTags = []string{"status_code"} checkOutput(t, &acc, expectedFields, expectedTags, absentFields, absentTags) } func TestContentLength(t *testing.T) { mux := setUpTestMux() ts := httptest.NewServer(mux) defer ts.Close() h := &HTTPResponse{ Log: testutil.Logger{}, URLs: []string{ts.URL + "/good"}, Body: "{ 'test': 'data'}", Method: "GET", ResponseTimeout: config.Duration(time.Second * 20), Headers: map[string]string{ "Content-Type": "application/json", }, FollowRedirects: true, } var acc testutil.Accumulator require.NoError(t, h.Init()) require.NoError(t, h.Gather(&acc)) expectedFields := map[string]interface{}{ "http_response_code": http.StatusOK, "result_type": "success", "result_code": 0, "response_time": nil, "content_length": len([]byte("hit the good page!")), } expectedTags := map[string]interface{}{ "server": nil, "method": "GET", "status_code": "200", "result": "success", } absentFields := []string{"response_string_match"} checkOutput(t, &acc, expectedFields, expectedTags, absentFields, nil) h = &HTTPResponse{ Log: testutil.Logger{}, URLs: []string{ts.URL + "/musthaveabody"}, Body: "{ 'test': 'data'}", Method: "GET", ResponseTimeout: config.Duration(time.Second * 20), Headers: map[string]string{ "Content-Type": "application/json", }, FollowRedirects: true, } acc = testutil.Accumulator{} require.NoError(t, h.Init()) require.NoError(t, h.Gather(&acc)) expectedFields = map[string]interface{}{ "http_response_code": http.StatusOK, "result_type": "success", "result_code": 0, "response_time": nil, "content_length": len([]byte("sent a body!")), } expectedTags = map[string]interface{}{ "server": nil, "method": "GET", "status_code": "200", "result": "success", } absentFields = []string{"response_string_match"} checkOutput(t, &acc, expectedFields, expectedTags, absentFields, nil) } func TestRedirect(t *testing.T) { ts := httptest.NewServer(http.NotFoundHandler()) defer ts.Close() ts.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.Header().Add("Location", "http://example.org") w.WriteHeader(http.StatusMovedPermanently) if _, err := w.Write([]byte("test")); err != nil { w.WriteHeader(http.StatusInternalServerError) t.Error(err) return } }) h := &HTTPResponse{ URLs: []string{ts.URL}, ResponseStringMatch: "test", } var acc testutil.Accumulator require.NoError(t, h.Init()) require.NoError(t, h.Gather(&acc)) expected := []telegraf.Metric{ testutil.MustMetric( "http_response", map[string]string{ "server": ts.URL, "method": "GET", "result": "success", "status_code": "301", }, map[string]interface{}{ "result_code": 0, "result_type": "success", "http_response_code": 301, "response_string_match": 1, "content_length": 4, }, time.Unix(0, 0), ), } actual := acc.GetTelegrafMetrics() for _, m := range actual { m.RemoveField("response_time") } testutil.RequireMetricsEqual(t, expected, actual, testutil.IgnoreTime()) } func TestBasicAuth(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if aHeader := r.Header.Get("Authorization"); aHeader != "Basic bWU6bXlwYXNzd29yZA==" { w.WriteHeader(http.StatusInternalServerError) t.Errorf("Not equal, expected: %q, actual: %q", "Basic bWU6bXlwYXNzd29yZA==", aHeader) return } w.WriteHeader(http.StatusOK) })) defer ts.Close() h := &HTTPResponse{ Log: testutil.Logger{}, URLs: []string{ts.URL + "/good"}, Body: "{ 'test': 'data'}", Method: "GET", ResponseTimeout: config.Duration(time.Second * 20), Username: config.NewSecret([]byte("me")), Password: config.NewSecret([]byte("mypassword")), Headers: map[string]string{ "Content-Type": "application/json", }, } var acc testutil.Accumulator require.NoError(t, h.Init()) require.NoError(t, h.Gather(&acc)) expectedFields := map[string]interface{}{ "http_response_code": http.StatusOK, "result_type": "success", "result_code": 0, "response_time": nil, "content_length": nil, } expectedTags := map[string]interface{}{ "server": nil, "method": "GET", "status_code": "200", "result": "success", } absentFields := []string{"response_string_match"} checkOutput(t, &acc, expectedFields, expectedTags, absentFields, nil) } func TestStatusCodeMatchFail(t *testing.T) { mux := setUpTestMux() ts := httptest.NewServer(mux) defer ts.Close() h := &HTTPResponse{ Log: testutil.Logger{}, URLs: []string{ts.URL + "/nocontent"}, ResponseStatusCode: http.StatusOK, ResponseTimeout: config.Duration(time.Second * 20), } var acc testutil.Accumulator require.NoError(t, h.Init()) require.NoError(t, h.Gather(&acc)) expectedFields := map[string]interface{}{ "http_response_code": http.StatusNoContent, "response_status_code_match": 0, "result_type": "response_status_code_mismatch", "result_code": 6, "response_time": nil, "content_length": nil, } expectedTags := map[string]interface{}{ "server": nil, "method": http.MethodGet, "status_code": "204", "result": "response_status_code_mismatch", } checkOutput(t, &acc, expectedFields, expectedTags, nil, nil) } func TestStatusCodeMatch(t *testing.T) { mux := setUpTestMux() ts := httptest.NewServer(mux) defer ts.Close() h := &HTTPResponse{ Log: testutil.Logger{}, URLs: []string{ts.URL + "/nocontent"}, ResponseStatusCode: http.StatusNoContent, ResponseTimeout: config.Duration(time.Second * 20), } var acc testutil.Accumulator require.NoError(t, h.Init()) require.NoError(t, h.Gather(&acc)) expectedFields := map[string]interface{}{ "http_response_code": http.StatusNoContent, "response_status_code_match": 1, "result_type": "success", "result_code": 0, "response_time": nil, "content_length": nil, } expectedTags := map[string]interface{}{ "server": nil, "method": http.MethodGet, "status_code": "204", "result": "success", } checkOutput(t, &acc, expectedFields, expectedTags, nil, nil) } func TestStatusCodeAndStringMatch(t *testing.T) { mux := setUpTestMux() ts := httptest.NewServer(mux) defer ts.Close() h := &HTTPResponse{ Log: testutil.Logger{}, URLs: []string{ts.URL + "/good"}, ResponseStatusCode: http.StatusOK, ResponseStringMatch: "hit the good page", ResponseTimeout: config.Duration(time.Second * 20), } var acc testutil.Accumulator require.NoError(t, h.Init()) require.NoError(t, h.Gather(&acc)) expectedFields := map[string]interface{}{ "http_response_code": http.StatusOK, "response_status_code_match": 1, "response_string_match": 1, "result_type": "success", "result_code": 0, "response_time": nil, "content_length": nil, } expectedTags := map[string]interface{}{ "server": nil, "method": http.MethodGet, "status_code": "200", "result": "success", } checkOutput(t, &acc, expectedFields, expectedTags, nil, nil) } func TestStatusCodeAndStringMatchFail(t *testing.T) { mux := setUpTestMux() ts := httptest.NewServer(mux) defer ts.Close() h := &HTTPResponse{ Log: testutil.Logger{}, URLs: []string{ts.URL + "/nocontent"}, ResponseStatusCode: http.StatusOK, ResponseStringMatch: "hit the good page", ResponseTimeout: config.Duration(time.Second * 20), } var acc testutil.Accumulator require.NoError(t, h.Init()) require.NoError(t, h.Gather(&acc)) expectedFields := map[string]interface{}{ "http_response_code": http.StatusNoContent, "response_status_code_match": 0, "response_string_match": 0, "result_type": "response_status_code_mismatch", "result_code": 6, "response_time": nil, "content_length": nil, } expectedTags := map[string]interface{}{ "server": nil, "method": http.MethodGet, "status_code": "204", "result": "response_status_code_mismatch", } checkOutput(t, &acc, expectedFields, expectedTags, nil, nil) } func TestSNI(t *testing.T) { ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.TLS.ServerName != "super-special-hostname.example.com" { w.WriteHeader(http.StatusInternalServerError) t.Errorf("Not equal, expected: %q, actual: %q", "super-special-hostname.example.com", r.TLS.ServerName) return } w.WriteHeader(http.StatusOK) })) defer ts.Close() h := &HTTPResponse{ Log: testutil.Logger{}, URLs: []string{ts.URL + "/good"}, Method: "GET", ResponseTimeout: config.Duration(time.Second * 20), ClientConfig: tls.ClientConfig{ InsecureSkipVerify: true, ServerName: "super-special-hostname.example.com", }, } var acc testutil.Accumulator require.NoError(t, h.Init()) require.NoError(t, h.Gather(&acc)) expectedFields := map[string]interface{}{ "http_response_code": http.StatusOK, "result_type": "success", "result_code": 0, "response_time": nil, "content_length": nil, } expectedTags := map[string]interface{}{ "server": nil, "method": "GET", "status_code": "200", "result": "success", } absentFields := []string{"response_string_match"} checkOutput(t, &acc, expectedFields, expectedTags, absentFields, nil) } func Test_isURLInIPv6(t *testing.T) { tests := []struct { address url.URL want bool }{ { address: parseURL(t, "http://[2001:db8:a0b:12f0::1]/index.html"), want: true, }, { address: parseURL(t, "http://[2001:db8:a0b:12f0::1]:80/index.html"), want: true, }, { address: parseURL(t, "https://[2001:db8:a0b:12f0::1%25eth0]:15000/"), // `%25` escapes `%` want: true, }, { address: parseURL(t, "https://2001:0db8:0001:0000:0000:0ab9:C0A8:0102"), want: true, }, { address: parseURL(t, "http://[2607:f8b0:4005:802::1007]/"), want: true, }, { address: parseURL(t, "https://127.0.0.1"), want: false, }, { address: parseURL(t, "https://google.com"), want: false, }, { address: parseURL(t, "https://thispagemayexist.ornot/index.html"), want: false, }, } for _, tt := range tests { t.Run(tt.address.String(), func(t *testing.T) { if got, _ := isURLInIPv6(tt.address); got != tt.want { t.Errorf("isURLInIPv6() = %v, want %v", got, tt.want) } }) } } func Test_isIPNetInIPv6(t *testing.T) { tests := []struct { address *net.IPNet want bool }{ { address: &net.IPNet{ IP: net.IPv4(127, 0, 0, 1), Mask: net.CIDRMask(8, 32), }, want: false, }, { address: &net.IPNet{ IP: net.IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, Mask: net.CIDRMask(128, 128), }, want: true, }, { address: &net.IPNet{ IP: net.IPv4(192, 168, 0, 1), Mask: net.CIDRMask(24, 32), }, want: false, }, { address: &net.IPNet{ IP: net.ParseIP("fe80::43ac:7835:471a:faba"), Mask: net.CIDRMask(64, 128), }, want: true, }, } for _, tt := range tests { t.Run(tt.address.String(), func(t *testing.T) { if got := isIPNetInIPv6(tt.address); got != tt.want { t.Errorf("isIPNetInIPv6() = %v, want %v", got, tt.want) } }) } } func parseURL(t *testing.T, address string) url.URL { u, err := url.Parse(address) require.NoError(t, err) require.NotNil(t, u) return *u }