github.com/Jeffail/benthos/v3@v3.65.0/lib/output/writer/http_client_test.go (about)

     1  package writer
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"io"
     7  	"mime"
     8  	"mime/multipart"
     9  	"net/http"
    10  	"net/http/httptest"
    11  	"strings"
    12  	"sync/atomic"
    13  	"testing"
    14  	"time"
    15  
    16  	"github.com/Jeffail/benthos/v3/lib/log"
    17  	"github.com/Jeffail/benthos/v3/lib/message"
    18  	"github.com/Jeffail/benthos/v3/lib/message/roundtrip"
    19  	"github.com/Jeffail/benthos/v3/lib/metrics"
    20  	"github.com/Jeffail/benthos/v3/lib/types"
    21  	"github.com/stretchr/testify/assert"
    22  	"github.com/stretchr/testify/require"
    23  )
    24  
    25  //------------------------------------------------------------------------------
    26  
    27  func TestHTTPClientRetries(t *testing.T) {
    28  	var reqCount uint32
    29  	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    30  		atomic.AddUint32(&reqCount, 1)
    31  		http.Error(w, "test error", http.StatusForbidden)
    32  	}))
    33  	defer ts.Close()
    34  
    35  	conf := NewHTTPClientConfig()
    36  	conf.URL = ts.URL + "/testpost"
    37  	conf.Retry = "1ms"
    38  	conf.NumRetries = 3
    39  
    40  	h, err := NewHTTPClient(conf, types.NoopMgr(), log.Noop(), metrics.Noop())
    41  	if err != nil {
    42  		t.Fatal(err)
    43  	}
    44  
    45  	if err = h.Write(message.New([][]byte{[]byte("test")})); err == nil {
    46  		t.Error("Expected error from end of retries")
    47  	}
    48  
    49  	if exp, act := uint32(4), atomic.LoadUint32(&reqCount); exp != act {
    50  		t.Errorf("Wrong count of HTTP attempts: %v != %v", exp, act)
    51  	}
    52  
    53  	h.CloseAsync()
    54  	if err = h.WaitForClose(time.Second); err != nil {
    55  		t.Error(err)
    56  	}
    57  }
    58  
    59  func TestHTTPClientBasic(t *testing.T) {
    60  	nTestLoops := 1000
    61  
    62  	resultChan := make(chan types.Message, 1)
    63  	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    64  		msg := message.New(nil)
    65  		defer func() {
    66  			resultChan <- msg
    67  		}()
    68  
    69  		b, err := io.ReadAll(r.Body)
    70  		if err != nil {
    71  			t.Error(err)
    72  			return
    73  		}
    74  		msg.Append(message.NewPart(b))
    75  	}))
    76  	defer ts.Close()
    77  
    78  	conf := NewHTTPClientConfig()
    79  	conf.URL = ts.URL + "/testpost"
    80  
    81  	h, err := NewHTTPClient(conf, types.NoopMgr(), log.Noop(), metrics.Noop())
    82  	if err != nil {
    83  		t.Fatal(err)
    84  	}
    85  
    86  	for i := 0; i < nTestLoops; i++ {
    87  		testStr := fmt.Sprintf("test%v", i)
    88  		testMsg := message.New([][]byte{[]byte(testStr)})
    89  
    90  		if err = h.Write(testMsg); err != nil {
    91  			t.Error(err)
    92  		}
    93  
    94  		select {
    95  		case resMsg := <-resultChan:
    96  			if resMsg.Len() != 1 {
    97  				t.Errorf("Wrong # parts: %v != %v", resMsg.Len(), 1)
    98  				return
    99  			}
   100  			if exp, actual := testStr, string(resMsg.Get(0).Get()); exp != actual {
   101  				t.Errorf("Wrong result, %v != %v", exp, actual)
   102  				return
   103  			}
   104  		case <-time.After(time.Second):
   105  			t.Errorf("Action timed out")
   106  			return
   107  		}
   108  	}
   109  
   110  	h.CloseAsync()
   111  	if err = h.WaitForClose(time.Second); err != nil {
   112  		t.Error(err)
   113  	}
   114  }
   115  
   116  func TestHTTPClientSyncResponse(t *testing.T) {
   117  	nTestLoops := 1000
   118  
   119  	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   120  		b, err := io.ReadAll(r.Body)
   121  		if err != nil {
   122  			t.Error(err)
   123  			return
   124  		}
   125  		w.Header().Add("fooheader", "foovalue")
   126  		w.Write([]byte("echo: "))
   127  		w.Write(b)
   128  	}))
   129  	defer ts.Close()
   130  
   131  	conf := NewHTTPClientConfig()
   132  	conf.URL = ts.URL + "/testpost"
   133  	conf.PropagateResponse = true
   134  
   135  	h, err := NewHTTPClient(conf, types.NoopMgr(), log.Noop(), metrics.Noop())
   136  	if err != nil {
   137  		t.Fatal(err)
   138  	}
   139  
   140  	for i := 0; i < nTestLoops; i++ {
   141  		testStr := fmt.Sprintf("test%v", i)
   142  
   143  		resultStore := roundtrip.NewResultStore()
   144  		testMsg := message.New([][]byte{[]byte(testStr)})
   145  		roundtrip.AddResultStore(testMsg, resultStore)
   146  
   147  		require.NoError(t, h.Write(testMsg))
   148  		resMsgs := resultStore.Get()
   149  		require.Len(t, resMsgs, 1)
   150  
   151  		resMsg := resMsgs[0]
   152  		require.Equal(t, 1, resMsg.Len())
   153  		assert.Equal(t, "echo: "+testStr, string(resMsg.Get(0).Get()))
   154  		assert.Equal(t, "", resMsg.Get(0).Metadata().Get("fooheader"))
   155  	}
   156  
   157  	h.CloseAsync()
   158  	if err = h.WaitForClose(time.Second); err != nil {
   159  		t.Error(err)
   160  	}
   161  }
   162  
   163  func TestHTTPClientSyncResponseCopyHeaders(t *testing.T) {
   164  	nTestLoops := 1000
   165  
   166  	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   167  		b, err := io.ReadAll(r.Body)
   168  		if err != nil {
   169  			t.Error(err)
   170  			return
   171  		}
   172  		w.Header().Add("fooheader", "foovalue")
   173  		w.Write([]byte("echo: "))
   174  		w.Write(b)
   175  	}))
   176  	defer ts.Close()
   177  
   178  	conf := NewHTTPClientConfig()
   179  	conf.URL = ts.URL + "/testpost"
   180  	conf.PropagateResponse = true
   181  	conf.CopyResponseHeaders = true
   182  
   183  	h, err := NewHTTPClient(conf, types.NoopMgr(), log.Noop(), metrics.Noop())
   184  	if err != nil {
   185  		t.Fatal(err)
   186  	}
   187  
   188  	for i := 0; i < nTestLoops; i++ {
   189  		testStr := fmt.Sprintf("test%v", i)
   190  
   191  		resultStore := roundtrip.NewResultStore()
   192  		testMsg := message.New([][]byte{[]byte(testStr)})
   193  		roundtrip.AddResultStore(testMsg, resultStore)
   194  
   195  		require.NoError(t, h.Write(testMsg))
   196  		resMsgs := resultStore.Get()
   197  		require.Len(t, resMsgs, 1)
   198  
   199  		resMsg := resMsgs[0]
   200  		require.Equal(t, 1, resMsg.Len())
   201  		assert.Equal(t, "echo: "+testStr, string(resMsg.Get(0).Get()))
   202  		assert.Equal(t, "foovalue", resMsg.Get(0).Metadata().Get("fooheader"))
   203  	}
   204  
   205  	h.CloseAsync()
   206  	if err = h.WaitForClose(time.Second); err != nil {
   207  		t.Error(err)
   208  	}
   209  }
   210  
   211  func TestHTTPClientMultipart(t *testing.T) {
   212  	nTestLoops := 1000
   213  
   214  	resultChan := make(chan types.Message, 1)
   215  	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   216  		msg := message.New(nil)
   217  		defer func() {
   218  			resultChan <- msg
   219  		}()
   220  
   221  		mediaType, params, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
   222  		if err != nil {
   223  			t.Errorf("Bad media type: %v -> %v", r.Header.Get("Content-Type"), err)
   224  			return
   225  		}
   226  
   227  		if strings.HasPrefix(mediaType, "multipart/") {
   228  			mr := multipart.NewReader(r.Body, params["boundary"])
   229  			for {
   230  				p, err := mr.NextPart()
   231  				if err == io.EOF {
   232  					break
   233  				}
   234  				if err != nil {
   235  					t.Error(err)
   236  					return
   237  				}
   238  				msgBytes, err := io.ReadAll(p)
   239  				if err != nil {
   240  					t.Error(err)
   241  					return
   242  				}
   243  				msg.Append(message.NewPart(msgBytes))
   244  			}
   245  		} else {
   246  			b, err := io.ReadAll(r.Body)
   247  			if err != nil {
   248  				t.Error(err)
   249  				return
   250  			}
   251  			msg.Append(message.NewPart(b))
   252  		}
   253  	}))
   254  	defer ts.Close()
   255  
   256  	conf := NewHTTPClientConfig()
   257  	conf.URL = ts.URL + "/testpost"
   258  
   259  	h, err := NewHTTPClient(conf, types.NoopMgr(), log.Noop(), metrics.Noop())
   260  	if err != nil {
   261  		t.Fatal(err)
   262  	}
   263  
   264  	for i := 0; i < nTestLoops; i++ {
   265  		testStr := fmt.Sprintf("test%v", i)
   266  		testMsg := message.New([][]byte{
   267  			[]byte(testStr + "PART-A"),
   268  			[]byte(testStr + "PART-B"),
   269  		})
   270  
   271  		if err = h.Write(testMsg); err != nil {
   272  			t.Error(err)
   273  		}
   274  
   275  		select {
   276  		case resMsg := <-resultChan:
   277  			if resMsg.Len() != 2 {
   278  				t.Errorf("Wrong # parts: %v != %v", resMsg.Len(), 2)
   279  				return
   280  			}
   281  			if exp, actual := testStr+"PART-A", string(resMsg.Get(0).Get()); exp != actual {
   282  				t.Errorf("Wrong result, %v != %v", exp, actual)
   283  				return
   284  			}
   285  			if exp, actual := testStr+"PART-B", string(resMsg.Get(1).Get()); exp != actual {
   286  				t.Errorf("Wrong result, %v != %v", exp, actual)
   287  				return
   288  			}
   289  		case <-time.After(time.Second):
   290  			t.Errorf("Action timed out")
   291  			return
   292  		}
   293  	}
   294  
   295  	h.CloseAsync()
   296  	if err = h.WaitForClose(time.Second); err != nil {
   297  		t.Error(err)
   298  	}
   299  }
   300  func TestHTTPOutputClientMultipartBody(t *testing.T) {
   301  	nTestLoops := 1000
   302  	resultChan := make(chan types.Message, 1)
   303  	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   304  		msg := message.New(nil)
   305  		defer func() {
   306  			resultChan <- msg
   307  		}()
   308  
   309  		mediaType, params, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
   310  		if err != nil {
   311  			t.Errorf("Bad media type: %v -> %v", r.Header.Get("Content-Type"), err)
   312  			return
   313  		}
   314  
   315  		if strings.HasPrefix(mediaType, "multipart/") {
   316  			mr := multipart.NewReader(r.Body, params["boundary"])
   317  			for {
   318  				p, err := mr.NextPart()
   319  
   320  				if err == io.EOF {
   321  					break
   322  				}
   323  				if err != nil {
   324  					t.Error(err)
   325  					return
   326  				}
   327  				msgBytes, err := io.ReadAll(p)
   328  				if err != nil {
   329  					t.Error(err)
   330  					return
   331  				}
   332  				msg.Append(message.NewPart(msgBytes))
   333  			}
   334  		}
   335  	}))
   336  	defer ts.Close()
   337  
   338  	conf := NewHTTPClientConfig()
   339  	conf.URL = ts.URL + "/testpost"
   340  	conf.Multipart = []HTTPClientMultipartExpression{
   341  		{
   342  			ContentDisposition: `form-data; name="text"`,
   343  			ContentType:        "text/plain",
   344  			Body:               "PART-A"},
   345  		{
   346  			ContentDisposition: `form-data; name="file1"; filename="a.txt"`,
   347  			ContentType:        "text/plain",
   348  			Body:               "PART-B"},
   349  	}
   350  	h, err := NewHTTPClient(conf, types.NoopMgr(), log.Noop(), metrics.Noop())
   351  	if err != nil {
   352  		t.Fatal(err)
   353  	}
   354  	for i := 0; i < nTestLoops; i++ {
   355  		if err = h.Write(message.New([][]byte{[]byte("test")})); err != nil {
   356  			t.Error(err)
   357  		}
   358  		select {
   359  		case resMsg := <-resultChan:
   360  			if resMsg.Len() != len(conf.Multipart) {
   361  				t.Errorf("Wrong # parts: %v != %v", resMsg.Len(), 2)
   362  				return
   363  			}
   364  			if exp, actual := "PART-A", string(resMsg.Get(0).Get()); exp != actual {
   365  				t.Errorf("Wrong result, %v != %v", exp, actual)
   366  				return
   367  			}
   368  			if exp, actual := "PART-B", string(resMsg.Get(1).Get()); exp != actual {
   369  				t.Errorf("Wrong result, %v != %v", exp, actual)
   370  				return
   371  			}
   372  		case <-time.After(time.Second):
   373  			t.Errorf("Action timed out")
   374  			return
   375  		}
   376  	}
   377  
   378  	h.CloseAsync()
   379  	if err = h.WaitForClose(time.Second); err != nil {
   380  		t.Error(err)
   381  	}
   382  }
   383  
   384  func TestHTTPOutputClientMultipartHeaders(t *testing.T) {
   385  	resultChan := make(chan types.Message, 1)
   386  	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   387  		msg := message.New(nil)
   388  		defer func() {
   389  			resultChan <- msg
   390  		}()
   391  
   392  		mediaType, params, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
   393  		if err != nil {
   394  			t.Errorf("Bad media type: %v -> %v", r.Header.Get("Content-Type"), err)
   395  			return
   396  		}
   397  
   398  		if strings.HasPrefix(mediaType, "multipart/") {
   399  			mr := multipart.NewReader(r.Body, params["boundary"])
   400  			for {
   401  				p, err := mr.NextPart()
   402  
   403  				if err == io.EOF {
   404  					break
   405  				}
   406  				if err != nil {
   407  					t.Error(err)
   408  					return
   409  				}
   410  				a, err := json.Marshal(p.Header)
   411  				if err != nil {
   412  					t.Error(err)
   413  					return
   414  				}
   415  				msg.Append(message.NewPart(a))
   416  			}
   417  		}
   418  	}))
   419  	defer ts.Close()
   420  
   421  	conf := NewHTTPClientConfig()
   422  	conf.URL = ts.URL + "/testpost"
   423  	conf.Multipart = []HTTPClientMultipartExpression{
   424  		{
   425  			ContentDisposition: `form-data; name="text"`,
   426  			ContentType:        "text/plain",
   427  			Body:               "PART-A"},
   428  		{
   429  			ContentDisposition: `form-data; name="file1"; filename="a.txt"`,
   430  			ContentType:        "text/plain",
   431  			Body:               "PART-B"},
   432  	}
   433  	h, err := NewHTTPClient(conf, types.NoopMgr(), log.Noop(), metrics.Noop())
   434  	if err != nil {
   435  		t.Fatal(err)
   436  	}
   437  	if err = h.Write(message.New([][]byte{[]byte("test")})); err != nil {
   438  		t.Error(err)
   439  	}
   440  	select {
   441  	case resMsg := <-resultChan:
   442  		for i := range conf.Multipart {
   443  			if resMsg.Len() != len(conf.Multipart) {
   444  				t.Errorf("Wrong # parts: %v != %v", resMsg.Len(), 2)
   445  				return
   446  			}
   447  			mp := make(map[string][]string)
   448  			err := json.Unmarshal(resMsg.Get(i).Get(), &mp)
   449  			if err != nil {
   450  				t.Error(err)
   451  			}
   452  			if exp, actual := conf.Multipart[i].ContentDisposition, mp["Content-Disposition"]; exp != actual[0] {
   453  				t.Errorf("Wrong result, %v != %v", exp, actual)
   454  				return
   455  			}
   456  			if exp, actual := conf.Multipart[i].ContentType, mp["Content-Type"]; exp != actual[0] {
   457  				t.Errorf("Wrong result, %v != %v", exp, actual)
   458  				return
   459  			}
   460  		}
   461  	case <-time.After(time.Second):
   462  		t.Errorf("Action timed out")
   463  		return
   464  
   465  	}
   466  	h.CloseAsync()
   467  	if err = h.WaitForClose(time.Second); err != nil {
   468  		t.Error(err)
   469  	}
   470  }