package http_response import ( "errors" "fmt" "io/ioutil" "net" "net/http" "net/http/httptest" "testing" "time" "github.com/influxdata/telegraf" "github.com/influxdata/telegraf/internal" "github.com/influxdata/telegraf/testutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // 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.Equal(t, field, value) 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() mux.HandleFunc("/redirect", func(w http.ResponseWriter, req *http.Request) { http.Redirect(w, req, "/good", http.StatusMovedPermanently) }) mux.HandleFunc("/good", func(w http.ResponseWriter, req *http.Request) { fmt.Fprintf(w, "hit the good page!") }) mux.HandleFunc("/jsonresponse", func(w http.ResponseWriter, req *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 := ioutil.ReadAll(req.Body) req.Body.Close() if err != nil { http.Error(w, "couldn't read request body", http.StatusBadRequest) return } if string(body) == "" { http.Error(w, "body was empty", http.StatusBadRequest) return } fmt.Fprintf(w, "sent a body!") }) mux.HandleFunc("/twosecondnap", func(w http.ResponseWriter, req *http.Request) { time.Sleep(time.Second * 2) return }) return mux } func checkOutput(t *testing.T, acc *testutil.Accumulator, presentFields map[string]interface{}, presentTags map[string]interface{}, absentFields []string, 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) { cHeader := r.Header.Get("Content-Type") assert.Equal(t, "Hello", r.Host) assert.Equal(t, "application/json", cHeader) w.WriteHeader(http.StatusOK) })) defer ts.Close() h := &HTTPResponse{ Log: testutil.Logger{}, Address: ts.URL, Method: "GET", ResponseTimeout: internal.Duration{Duration: time.Second * 2}, Headers: map[string]string{ "Content-Type": "application/json", "Host": "Hello", }, } var acc testutil.Accumulator err := h.Gather(&acc) require.NoError(t, err) 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{}, Address: ts.URL + "/good", Body: "{ 'test': 'data'}", Method: "GET", ResponseTimeout: internal.Duration{Duration: time.Second * 20}, Headers: map[string]string{ "Content-Type": "application/json", }, FollowRedirects: true, } var acc testutil.Accumulator err := h.Gather(&acc) require.NoError(t, err) 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 findInterface() (net.Interface, error) { potential, _ := net.Interfaces() 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, _ := i.Addrs(); len(addrs) > 0 { // return interface if it has at least one unicast address 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{}, Address: ts.URL + "/good", Body: "{ 'test': 'data'}", Method: "GET", ResponseTimeout: internal.Duration{Duration: time.Second * 20}, Headers: map[string]string{ "Content-Type": "application/json", }, FollowRedirects: true, Interface: intf.Name, } var acc testutil.Accumulator err = h.Gather(&acc) require.NoError(t, err) 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{}, Address: ts.URL + "/redirect", Body: "{ 'test': 'data'}", Method: "GET", ResponseTimeout: internal.Duration{Duration: time.Second * 20}, Headers: map[string]string{ "Content-Type": "application/json", }, FollowRedirects: true, } var acc testutil.Accumulator err := h.Gather(&acc) require.NoError(t, err) 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{}, Address: ts.URL + "/badredirect", Body: "{ 'test': 'data'}", Method: "GET", ResponseTimeout: internal.Duration{Duration: time.Second * 20}, Headers: map[string]string{ "Content-Type": "application/json", }, FollowRedirects: true, } acc = testutil.Accumulator{} err = h.Gather(&acc) require.NoError(t, err) 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{}, Address: ts.URL + "/mustbepostmethod", Body: "{ 'test': 'data'}", Method: "POST", ResponseTimeout: internal.Duration{Duration: time.Second * 20}, Headers: map[string]string{ "Content-Type": "application/json", }, FollowRedirects: true, } var acc testutil.Accumulator err := h.Gather(&acc) require.NoError(t, err) 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{}, Address: ts.URL + "/mustbepostmethod", Body: "{ 'test': 'data'}", Method: "GET", ResponseTimeout: internal.Duration{Duration: time.Second * 20}, Headers: map[string]string{ "Content-Type": "application/json", }, FollowRedirects: true, } acc = testutil.Accumulator{} err = h.Gather(&acc) require.NoError(t, err) 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{}, Address: ts.URL + "/mustbepostmethod", Body: "{ 'test': 'data'}", Method: "head", ResponseTimeout: internal.Duration{Duration: time.Second * 20}, Headers: map[string]string{ "Content-Type": "application/json", }, FollowRedirects: true, } acc = testutil.Accumulator{} err = h.Gather(&acc) require.NoError(t, err) 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{}, Address: ts.URL + "/musthaveabody", Body: "{ 'test': 'data'}", Method: "GET", ResponseTimeout: internal.Duration{Duration: time.Second * 20}, Headers: map[string]string{ "Content-Type": "application/json", }, FollowRedirects: true, } var acc testutil.Accumulator err := h.Gather(&acc) require.NoError(t, err) 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{}, Address: ts.URL + "/musthaveabody", Method: "GET", ResponseTimeout: internal.Duration{Duration: time.Second * 20}, Headers: map[string]string{ "Content-Type": "application/json", }, FollowRedirects: true, } acc = testutil.Accumulator{} err = h.Gather(&acc) require.NoError(t, err) 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{}, Address: ts.URL + "/good", Body: "{ 'test': 'data'}", Method: "GET", ResponseStringMatch: "hit the good page", ResponseTimeout: internal.Duration{Duration: time.Second * 20}, Headers: map[string]string{ "Content-Type": "application/json", }, FollowRedirects: true, } var acc testutil.Accumulator err := h.Gather(&acc) require.NoError(t, err) 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{}, Address: ts.URL + "/jsonresponse", Body: "{ 'test': 'data'}", Method: "GET", ResponseStringMatch: "\"service_status\": \"up\"", ResponseTimeout: internal.Duration{Duration: time.Second * 20}, Headers: map[string]string{ "Content-Type": "application/json", }, FollowRedirects: true, } var acc testutil.Accumulator err := h.Gather(&acc) require.NoError(t, err) 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{}, Address: ts.URL + "/good", Body: "{ 'test': 'data'}", Method: "GET", ResponseStringMatch: "hit the bad page", ResponseTimeout: internal.Duration{Duration: time.Second * 20}, Headers: map[string]string{ "Content-Type": "application/json", }, FollowRedirects: true, } var acc testutil.Accumulator err := h.Gather(&acc) require.NoError(t, err) 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{}, Address: ts.URL + "/twosecondnap", Body: "{ 'test': 'data'}", Method: "GET", ResponseTimeout: internal.Duration{Duration: time.Second}, Headers: map[string]string{ "Content-Type": "application/json", }, FollowRedirects: true, } var acc testutil.Accumulator err := h.Gather(&acc) require.NoError(t, err) 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{}, Address: ts.URL + "/good", Body: "{ 'test': 'data'}", Method: "GET", ResponseStringMatch: "bad regex:[[", ResponseTimeout: internal.Duration{Duration: time.Second * 20}, Headers: map[string]string{ "Content-Type": "application/json", }, FollowRedirects: true, } var acc testutil.Accumulator err := h.Gather(&acc) require.Error(t, err) absentFields := []string{"http_response_code", "response_time", "content_length", "response_string_match", "result_type", "result_code"} absentTags := []string{"status_code", "result", "server", "method"} checkOutput(t, &acc, nil, nil, absentFields, absentTags) } func TestNetworkErrors(t *testing.T) { // DNS error h := &HTTPResponse{ Log: testutil.Logger{}, Address: "https://nonexistent.nonexistent", // Any non-resolvable URL works here Body: "", Method: "GET", ResponseTimeout: internal.Duration{Duration: time.Second * 20}, FollowRedirects: false, } var acc testutil.Accumulator err := h.Gather(&acc) require.NoError(t, err) 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{}, Address: "https:/nonexistent.nonexistent", // Any non-routable IP works here Body: "", Method: "GET", ResponseTimeout: internal.Duration{Duration: time.Second * 5}, FollowRedirects: false, } acc = testutil.Accumulator{} err = h.Gather(&acc) require.NoError(t, err) 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: internal.Duration{Duration: time.Second * 20}, Headers: map[string]string{ "Content-Type": "application/json", }, FollowRedirects: true, } var acc testutil.Accumulator err := h.Gather(&acc) require.NoError(t, err) 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: internal.Duration{Duration: time.Second * 20}, Headers: map[string]string{ "Content-Type": "application/json", }, FollowRedirects: true, } acc = testutil.Accumulator{} err = h.Gather(&acc) require.NoError(t, err) 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, r *http.Request) { w.Header().Add("Location", "http://example.org") w.WriteHeader(http.StatusMovedPermanently) w.Write([]byte("test")) }) plugin := &HTTPResponse{ URLs: []string{ts.URL}, ResponseStringMatch: "test", } var acc testutil.Accumulator err := plugin.Gather(&acc) require.NoError(t, err) 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) { aHeader := r.Header.Get("Authorization") assert.Equal(t, "Basic bWU6bXlwYXNzd29yZA==", aHeader) w.WriteHeader(http.StatusOK) })) defer ts.Close() h := &HTTPResponse{ Log: testutil.Logger{}, Address: ts.URL + "/good", Body: "{ 'test': 'data'}", Method: "GET", ResponseTimeout: internal.Duration{Duration: time.Second * 20}, Username: "me", Password: "mypassword", Headers: map[string]string{ "Content-Type": "application/json", }, } var acc testutil.Accumulator err := h.Gather(&acc) require.NoError(t, err) 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) }