// SPDX-License-Identifier: Apache-2.0 // // The OpenSearch Contributors require contributions made to // this file be licensed under the Apache-2.0 license or a // compatible open source license. // // Modifications Copyright OpenSearch Contributors. See // GitHub history for details. // Licensed to Elasticsearch B.V. under one or more contributor // license agreements. See the NOTICE file distributed with // this work for additional information regarding copyright // ownership. Elasticsearch B.V. licenses this file to you under // the Apache License, Version 2.0 (the "License"); you may // not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. //go:build !integration package opensearchtransport import ( "encoding/json" "errors" "fmt" "io" "net/http" "net/url" "os" "regexp" "strings" "sync" "testing" "time" ) var ( _ = fmt.Print _ = os.Stdout ) func TestTransportLogger(t *testing.T) { newRoundTripper := func() http.RoundTripper { return &mockTransp{ RoundTripFunc: func(req *http.Request) (*http.Response, error) { return &http.Response{ Status: fmt.Sprintf("%d %s", http.StatusOK, http.StatusText(http.StatusOK)), StatusCode: http.StatusOK, ContentLength: 13, Header: http.Header(map[string][]string{"Content-Type": {"application/json"}}), Body: io.NopCloser(strings.NewReader(`{"foo":"bar"}`)), }, nil }, } } t.Run("Defaults", func(t *testing.T) { var wg sync.WaitGroup tp, _ := New(Config{ URLs: []*url.URL{{Scheme: "http", Host: "foo"}}, Transport: newRoundTripper(), // Logger: io.Discard, }) for i := 0; i < 100; i++ { wg.Add(1) go func() { defer wg.Done() req, _ := http.NewRequest(http.MethodGet, "/abc", nil) resp, err := tp.Perform(req) if err != nil { t.Errorf("Unexpected error: %s", err) return } defer resp.Body.Close() }() } wg.Wait() }) t.Run("Nil", func(t *testing.T) { tp, _ := New(Config{ URLs: []*url.URL{{Scheme: "http", Host: "foo"}}, Transport: newRoundTripper(), Logger: nil, }) req, _ := http.NewRequest(http.MethodGet, "/abc", nil) resp, err := tp.Perform(req) if err != nil { t.Fatalf("Unexpected error: %s", err) } defer resp.Body.Close() }) t.Run("No HTTP response", func(t *testing.T) { tp, _ := New(Config{ URLs: []*url.URL{{Scheme: "http", Host: "foo"}}, Transport: &mockTransp{ RoundTripFunc: func(req *http.Request) (*http.Response, error) { return nil, errors.New("Mock error") }, }, Logger: &TextLogger{Output: io.Discard}, }) req, _ := http.NewRequest(http.MethodGet, "/abc", nil) resp, err := tp.Perform(req) if err == nil { defer resp.Body.Close() t.Errorf("Expected error: %v", err) } if resp != nil { t.Errorf("Expected nil response, got: %v", err) } }) t.Run("Keep response body", func(t *testing.T) { var dst strings.Builder tp, _ := New(Config{ URLs: []*url.URL{{Scheme: "http", Host: "foo"}}, Transport: newRoundTripper(), Logger: &TextLogger{Output: &dst, EnableRequestBody: true, EnableResponseBody: true}, }) req, _ := http.NewRequest(http.MethodGet, "/abc?q=a,b", nil) req.Body = io.NopCloser(strings.NewReader(`{"query":"42"}`)) res, err := tp.Perform(req) if err != nil { t.Fatalf("Unexpected error: %s", err) } defer res.Body.Close() body, err := io.ReadAll(res.Body) if err != nil { t.Fatalf("Error reading response body: %s", err) } if len(dst.String()) < 1 { t.Errorf("Log is empty: %#v", dst.String()) } if len(body) < 1 { t.Fatalf("Body is empty: %#v", body) } }) t.Run("Text with body", func(t *testing.T) { var dst strings.Builder tp, _ := New(Config{ URLs: []*url.URL{{Scheme: "http", Host: "foo"}}, Transport: newRoundTripper(), Logger: &TextLogger{Output: &dst, EnableRequestBody: true, EnableResponseBody: true}, }) req, _ := http.NewRequest(http.MethodGet, "/abc?q=a,b", nil) req.Body = io.NopCloser(strings.NewReader(`{"query":"42"}`)) res, err := tp.Perform(req) if err != nil { t.Fatalf("Unexpected error: %s", err) } defer res.Body.Close() _, err = io.ReadAll(res.Body) if err != nil { t.Fatalf("Error reading response body: %s", err) } output := dst.String() output = strings.TrimSuffix(output, "\n") // fmt.Println(output) lines := strings.Split(output, "\n") if len(lines) != 3 { t.Fatalf("Expected 3 lines, got %d", len(lines)) } if !strings.Contains(lines[0], "GET http://foo/abc?q=a,b") { t.Errorf("Unexpected output: %s", lines[0]) } if lines[1] != `> {"query":"42"}` { t.Errorf("Unexpected output: %s", lines[1]) } if lines[2] != `< {"foo":"bar"}` { t.Errorf("Unexpected output: %s", lines[1]) } }) t.Run("Color with body", func(t *testing.T) { var dst strings.Builder tp, _ := New(Config{ URLs: []*url.URL{{Scheme: "http", Host: "foo"}}, Transport: newRoundTripper(), Logger: &ColorLogger{Output: &dst, EnableRequestBody: true, EnableResponseBody: true}, }) req, _ := http.NewRequest(http.MethodGet, "/abc?q=a,b", nil) req.Body = io.NopCloser(strings.NewReader(`{"query":"42"}`)) res, err := tp.Perform(req) if err != nil { t.Fatalf("Unexpected error: %s", err) } defer res.Body.Close() _, err = io.ReadAll(res.Body) if err != nil { t.Fatalf("Error reading response body: %s", err) } var output string stripANSI := regexp.MustCompile("(?sm)\x1b\\[.+?m([^\x1b]+?)|\x1b\\[0m") for _, v := range strings.Split(dst.String(), "\n") { if v != "" { output += stripANSI.ReplaceAllString(v, "$1") if !strings.HasSuffix(output, "\n") { output += "\n" } } } output = strings.TrimSuffix(output, "\n") // fmt.Println(output) lines := strings.Split(output, "\n") if len(lines) != 4 { t.Fatalf("Expected 4 lines, got %d", len(lines)) } if !strings.Contains(lines[0], "GET http://foo/abc?q=a,b") { t.Errorf("Unexpected output: %s", lines[0]) } if !strings.Contains(lines[1], `» {"query":"42"}`) { t.Errorf("Unexpected output: %s", lines[1]) } if !strings.Contains(lines[2], `« {"foo":"bar"}`) { t.Errorf("Unexpected output: %s", lines[2]) } }) t.Run("Curl", func(t *testing.T) { var dst strings.Builder tp, _ := New(Config{ URLs: []*url.URL{{Scheme: "http", Host: "foo"}}, Transport: newRoundTripper(), Logger: &CurlLogger{Output: &dst, EnableRequestBody: true, EnableResponseBody: true}, }) req, _ := http.NewRequest(http.MethodGet, "/abc?q=a,b", nil) req.Body = io.NopCloser(strings.NewReader(`{"query":"42"}`)) res, err := tp.Perform(req) if err != nil { t.Fatalf("Unexpected error: %s", err) } defer res.Body.Close() _, err = io.ReadAll(res.Body) if err != nil { t.Fatalf("Error reading response body: %s", err) } output := dst.String() output = strings.TrimSuffix(output, "\n") lines := strings.Split(output, "\n") if len(lines) != 9 { t.Fatalf("Expected 9 lines, got %d", len(lines)) } if !strings.Contains(lines[0], "curl -X GET 'http://foo/abc?pretty&q=a%2Cb'") { t.Errorf("Unexpected output: %s", lines[0]) } }) t.Run("JSON", func(t *testing.T) { var dst strings.Builder tp, _ := New(Config{ URLs: []*url.URL{{Scheme: "http", Host: "foo"}}, Transport: newRoundTripper(), Logger: &JSONLogger{Output: &dst}, }) req, _ := http.NewRequest(http.MethodGet, "/abc?q=a,b", nil) req.Body = io.NopCloser(strings.NewReader(`{"query":"42"}`)) resp, err := tp.Perform(req) if err != nil { t.Fatalf("Unexpected error: %s", err) } defer resp.Body.Close() output := dst.String() output = strings.TrimSuffix(output, "\n") // fmt.Println(output) lines := strings.Split(output, "\n") if len(lines) != 1 { t.Fatalf("Expected 1 line, got %d", len(lines)) } var j map[string]interface{} if err := json.Unmarshal([]byte(output), &j); err != nil { t.Errorf("Error decoding JSON: %s", err) } domain := j["url"].(map[string]interface{})["domain"] if domain != "foo" { t.Errorf("Unexpected JSON output: %s", domain) } }) t.Run("JSON with request body", func(t *testing.T) { var dst strings.Builder tp, _ := New(Config{ URLs: []*url.URL{{Scheme: "http", Host: "foo"}}, Transport: newRoundTripper(), Logger: &JSONLogger{Output: &dst, EnableRequestBody: true}, }) req, _ := http.NewRequest(http.MethodGet, "/abc?q=a,b", nil) req.Body = io.NopCloser(strings.NewReader(`{"query":"42"}`)) res, err := tp.Perform(req) if err != nil { t.Fatalf("Unexpected error: %s", err) } defer res.Body.Close() _, err = io.ReadAll(res.Body) if err != nil { t.Fatalf("Error reading response body: %s", err) } output := dst.String() output = strings.TrimSuffix(output, "\n") // fmt.Println(output) lines := strings.Split(output, "\n") if len(lines) != 1 { t.Fatalf("Expected 1 line, got %d", len(lines)) } var j map[string]interface{} if err := json.Unmarshal([]byte(output), &j); err != nil { t.Errorf("Error decoding JSON: %s", err) } body := j["http"].(map[string]interface{})["request"].(map[string]interface{})["body"].(string) if !strings.Contains(body, "query") { t.Errorf("Unexpected JSON output: %s", body) } }) t.Run("Custom", func(t *testing.T) { var dst strings.Builder tp, _ := New(Config{ URLs: []*url.URL{{Scheme: "http", Host: "foo"}}, Transport: newRoundTripper(), Logger: &CustomLogger{Output: &dst}, }) req, _ := http.NewRequest(http.MethodGet, "/abc?q=a,b", nil) req.Body = io.NopCloser(strings.NewReader(`{"query":"42"}`)) res, err := tp.Perform(req) if err != nil { t.Fatalf("Unexpected error: %s", err) } defer res.Body.Close() if !strings.HasPrefix(dst.String(), "GET http://foo/abc?q=a,b") { t.Errorf("Unexpected output: %s", dst.String()) } }) t.Run("Duplicate body", func(t *testing.T) { input := ResponseBody{content: strings.NewReader("FOOBAR")} b1, b2, err := duplicateBody(&input) if err != nil { t.Fatalf("Unexpected error: %s", err) } if !input.closed { t.Errorf("Expected input to be closed: %#v", input) } read, _ := io.ReadAll(&input) if len(read) > 0 { t.Errorf("Expected input to be drained: %#v", input.content) } b1r, _ := io.ReadAll(b1) b2r, _ := io.ReadAll(b2) if len(b1r) != 6 || len(b2r) != 6 { t.Errorf( "Unexpected duplicate content, b1=%q (%db), b2=%q (%db)", string(b1r), len(b1r), string(b2r), len(b2r), ) } }) t.Run("Duplicate body with error", func(t *testing.T) { input := ResponseBody{content: &ErrorReader{r: strings.NewReader("FOOBAR")}} b1, b2, err := duplicateBody(&input) if err == nil { t.Errorf("Expected error, got: %v", err) } if err.Error() != "MOCK ERROR" { t.Errorf("Unexpected error value, expected [ERROR MOCK], got [%s]", err.Error()) } read, _ := io.ReadAll(&input) if string(read) != "BAR" { t.Errorf("Unexpected undrained part: %q", read) } b2r, _ := io.ReadAll(b2) if string(b2r) != "FOO" { t.Errorf("Unexpected value, b2=%q", string(b2r)) } b1c, err := io.ReadAll(b1) if string(b1c) != "FOO" { t.Errorf("Unexpected value, b1=%q", string(b1c)) } if err == nil { t.Errorf("Expected error when reading b1, got: %v", err) } if err.Error() != "MOCK ERROR" { t.Errorf("Unexpected error value, expected [ERROR MOCK], got [%s]", err.Error()) } }) } func TestDebuggingLogger(t *testing.T) { logger := &debuggingLogger{Output: io.Discard} t.Run("Log", func(t *testing.T) { if err := logger.Log("Foo"); err != nil { t.Errorf("Unexpected error: %s", err) } }) t.Run("Logf", func(t *testing.T) { if err := logger.Logf("Foo %d", 1); err != nil { t.Errorf("Unexpected error: %s", err) } }) } type CustomLogger struct { Output io.Writer } func (l *CustomLogger) LogRoundTrip( req *http.Request, res *http.Response, _ error, _ time.Time, _ time.Duration, ) error { fmt.Fprintln(l.Output, req.Method, req.URL, "->", res.Status) return nil } func (l *CustomLogger) RequestBodyEnabled() bool { return false } func (l *CustomLogger) ResponseBodyEnabled() bool { return false } type ResponseBody struct { content io.Reader closed bool } func (r *ResponseBody) Read(p []byte) (int, error) { return r.content.Read(p) } func (r *ResponseBody) Close() error { r.closed = true return nil } type ErrorReader struct { r io.Reader } func (r *ErrorReader) Read(p []byte) (int, error) { lr := io.LimitReader(r.r, 3) c, _ := lr.Read(p) return c, errors.New("MOCK ERROR") }