github.com/avenga/couper@v1.12.2/server/http_backend_test.go (about)

     1  package server_test
     2  
     3  import (
     4  	"bytes"
     5  	"compress/gzip"
     6  	"context"
     7  	"encoding/json"
     8  	"io"
     9  	"net/http"
    10  	"net/http/httptest"
    11  	"strconv"
    12  	"sync"
    13  	"sync/atomic"
    14  	"testing"
    15  	"time"
    16  
    17  	"github.com/sirupsen/logrus"
    18  
    19  	"github.com/avenga/couper/internal/test"
    20  	"github.com/avenga/couper/logging"
    21  )
    22  
    23  func TestBackend_MaxConnections(t *testing.T) {
    24  	helper := test.New(t)
    25  
    26  	const reqCount = 3
    27  	lastSeen := map[string]string{}
    28  	lastSeenMu := sync.Mutex{}
    29  	origin := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
    30  		lastSeenMu.Lock()
    31  		defer lastSeenMu.Unlock()
    32  
    33  		if lastSeen[r.URL.Path] != "" && lastSeen[r.URL.Path] != r.RemoteAddr {
    34  			t.Errorf("expected same remote addr for path: %q", r.URL.Path)
    35  			rw.WriteHeader(http.StatusInternalServerError)
    36  		} else {
    37  			rw.WriteHeader(http.StatusNoContent)
    38  		}
    39  		lastSeen[r.URL.Path] = r.RemoteAddr
    40  	}))
    41  
    42  	defer origin.Close()
    43  
    44  	shutdown, _, cerr := newCouperWithTemplate("testdata/integration/backends/03_couper.hcl", helper, map[string]interface{}{
    45  		"origin": origin.URL,
    46  	})
    47  	helper.Must(cerr)
    48  	defer shutdown()
    49  
    50  	paths := []string{
    51  		"/",
    52  		"/be",
    53  		"/fake-sequence",
    54  	}
    55  
    56  	originWait := sync.WaitGroup{}
    57  	originWait.Add(len(paths) * reqCount)
    58  	waitForCh := make(chan struct{})
    59  
    60  	client := test.NewHTTPClient()
    61  
    62  	for _, clientPath := range paths {
    63  		for i := 0; i < reqCount; i++ {
    64  			go func(path string) {
    65  				req, _ := http.NewRequest(http.MethodGet, "http://couper.dev:8080"+path, nil)
    66  				<-waitForCh
    67  				res, err := client.Do(req)
    68  				helper.Must(err)
    69  
    70  				if res.StatusCode != http.StatusNoContent {
    71  					t.Errorf("want: 204, got %d", res.StatusCode)
    72  				}
    73  
    74  				originWait.Done()
    75  			}(clientPath)
    76  		}
    77  	}
    78  
    79  	close(waitForCh)
    80  	originWait.Wait()
    81  }
    82  
    83  func TestBackend_MaxConnections_BodyClose(t *testing.T) {
    84  	helper := test.New(t)
    85  
    86  	origin := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
    87  		time.Sleep(time.Second) // always delay, ensures every req hit runs into max_conns issue
    88  
    89  		rw.Header().Set("Content-Type", "application/json")
    90  		b, err := json.Marshal(r.URL)
    91  		helper.Must(err)
    92  		_, err = rw.Write(b)
    93  		helper.Must(err)
    94  	}))
    95  
    96  	defer origin.Close()
    97  
    98  	shutdown, _, cerr := newCouperWithTemplate("testdata/integration/backends/04_couper.hcl", helper,
    99  		map[string]interface{}{
   100  			"origin": origin.URL,
   101  		})
   102  	helper.Must(cerr)
   103  	defer shutdown()
   104  
   105  	client := test.NewHTTPClient()
   106  
   107  	paths := []string{
   108  		"/",
   109  		"/named",
   110  		"/default",
   111  		"/default2",
   112  		"/ws",
   113  		"/proxy-seq",
   114  		"/proxy-seq-ref",
   115  	}
   116  
   117  	t.Run("parallel", func(t *testing.T) {
   118  		for _, path := range paths {
   119  			p := path // we need a local copy due to ref in parallel test func
   120  			t.Run("_"+p, func(st *testing.T) {
   121  				st.Parallel()
   122  
   123  				h := test.New(st)
   124  
   125  				req, _ := http.NewRequest(http.MethodGet, "http://couper.dev:8080"+p, nil)
   126  
   127  				deadline, cancel := context.WithTimeout(context.Background(), time.Second*time.Duration(len(paths)*10))
   128  				defer cancel()
   129  				res, err := client.Do(req.WithContext(deadline))
   130  				h.Must(err)
   131  
   132  				if res.StatusCode != http.StatusOK {
   133  					st.Errorf("want: 200, got %d", res.StatusCode)
   134  				}
   135  
   136  				_, err = io.Copy(io.Discard, res.Body)
   137  				h.Must(err)
   138  				h.Must(res.Body.Close())
   139  			})
   140  		}
   141  	})
   142  }
   143  
   144  // TestBackend_WithoutOrigin expects the listed errors to ensure no host from the client-request
   145  // leaks into the backend structure for connecting to the origin.
   146  func TestBackend_WithoutOrigin(t *testing.T) {
   147  	helper := test.New(t)
   148  	shutdown, hook := newCouper("testdata/integration/backends/01_couper.hcl", helper)
   149  	defer shutdown()
   150  
   151  	client := test.NewHTTPClient()
   152  
   153  	for _, tc := range []struct {
   154  		path    string
   155  		message string
   156  	}{
   157  		{"/proxy/backend-path", `configuration error: anonymous_6_13: the origin attribute has to contain an absolute URL with a valid hostname: ""`},
   158  		{"/proxy/url", `configuration error: anonymous_15_13: the origin attribute has to contain an absolute URL with a valid hostname: ""`},
   159  		{"/request/backend-path", `configuration error: anonymous_28_15: the origin attribute has to contain an absolute URL with a valid hostname: ""`},
   160  		{"/request/url", `configuration error: anonymous_37_15: the origin attribute has to contain an absolute URL with a valid hostname: ""`},
   161  	} {
   162  		t.Run(tc.path, func(st *testing.T) {
   163  			hook.Reset()
   164  
   165  			h := test.New(st)
   166  			req, _ := http.NewRequest(http.MethodGet, "http://couper.dev:8080"+tc.path, nil)
   167  			res, err := client.Do(req)
   168  			h.Must(err)
   169  
   170  			if res.StatusCode != http.StatusInternalServerError {
   171  				st.Errorf("want: 500, got %d", res.StatusCode)
   172  			}
   173  
   174  			for _, e := range hook.AllEntries() {
   175  				if e.Level != logrus.ErrorLevel {
   176  					continue
   177  				}
   178  
   179  				if e.Message != tc.message {
   180  					st.Errorf("\nwant: %q\ngot:  %q\n", tc.message, e.Message)
   181  				}
   182  			}
   183  
   184  		})
   185  
   186  	}
   187  }
   188  
   189  func TestBackend_LogResponseBytes(t *testing.T) {
   190  	helper := test.New(t)
   191  
   192  	var writtenBytes int64
   193  	origin := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
   194  		rw.Header().Set("Content-Type", "application/json")
   195  		b, err := json.Marshal(r.URL)
   196  		helper.Must(err)
   197  
   198  		if r.Header.Get("Accept-Encoding") == "gzip" {
   199  			buf := &bytes.Buffer{}
   200  			gw := gzip.NewWriter(buf)
   201  			_, zerr := gw.Write(b)
   202  			helper.Must(zerr)
   203  			helper.Must(gw.Close())
   204  
   205  			atomic.StoreInt64(&writtenBytes, int64(buf.Len()))
   206  
   207  			_, err = io.Copy(rw, buf)
   208  			helper.Must(err)
   209  		} else {
   210  			n, werr := rw.Write(b)
   211  			helper.Must(werr)
   212  			atomic.StoreInt64(&writtenBytes, int64(n))
   213  		}
   214  	}))
   215  
   216  	defer origin.Close()
   217  
   218  	shutdown, hook, cerr := newCouperWithTemplate("testdata/integration/backends/05_couper.hcl", helper,
   219  		map[string]interface{}{
   220  			"origin": origin.URL,
   221  		})
   222  	helper.Must(cerr)
   223  	defer shutdown()
   224  
   225  	client := test.NewHTTPClient()
   226  
   227  	cases := []struct {
   228  		accept string
   229  		path   string
   230  	}{
   231  		{path: "/"},
   232  		{accept: "gzip", path: "/zipped"},
   233  	}
   234  
   235  	for _, tc := range cases {
   236  		hook.Reset()
   237  
   238  		deadline, cancel := context.WithTimeout(context.Background(), time.Second*10)
   239  
   240  		req, _ := http.NewRequest(http.MethodGet, "http://couper.dev:8080"+tc.path, nil)
   241  
   242  		if tc.accept != "" {
   243  			req.Header.Set("Accept-Encoding", tc.accept)
   244  		}
   245  
   246  		res, err := client.Do(req.WithContext(deadline))
   247  		cancel()
   248  		helper.Must(err)
   249  
   250  		if res.StatusCode != http.StatusOK {
   251  			t.Errorf("want: 200, got %d", res.StatusCode)
   252  		}
   253  
   254  		_, err = io.Copy(io.Discard, res.Body)
   255  		helper.Must(err)
   256  
   257  		helper.Must(res.Body.Close())
   258  
   259  		var seen bool
   260  		for _, e := range hook.AllEntries() {
   261  			if e.Data["type"] != "couper_backend" {
   262  				continue
   263  			}
   264  
   265  			seen = true
   266  
   267  			response, ok := e.Data["response"]
   268  			if !ok {
   269  				t.Error("expected response log field")
   270  			}
   271  
   272  			bytesValue, bok := response.(logging.Fields)["bytes"]
   273  			if !bok {
   274  				t.Error("expected response.bytes log field")
   275  			}
   276  
   277  			expectedBytes := atomic.LoadInt64(&writtenBytes)
   278  			if bytesValue.(int64) != expectedBytes {
   279  				t.Errorf("bytes differs: want: %d, got: %d", expectedBytes, bytesValue)
   280  			}
   281  		}
   282  
   283  		if !seen {
   284  			t.Error("expected upstream log")
   285  		}
   286  	}
   287  }
   288  
   289  func TestBackend_Unhealthy(t *testing.T) {
   290  	helper := test.New(t)
   291  
   292  	var unhealthy int64
   293  	origin := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
   294  		if counter := r.Header.Get("Counter"); counter != "" {
   295  			c, _ := strconv.Atoi(counter)
   296  			if c > 2 {
   297  				atomic.StoreInt64(&unhealthy, 1)
   298  			}
   299  		}
   300  		if atomic.LoadInt64(&unhealthy) == 1 {
   301  			rw.WriteHeader(http.StatusConflict)
   302  		} else {
   303  			rw.WriteHeader(http.StatusNoContent)
   304  			time.Sleep(time.Second / 3)
   305  		}
   306  	}))
   307  
   308  	defer origin.Close()
   309  
   310  	shutdown, _, cerr := newCouperWithTemplate("testdata/integration/backends/06_couper.hcl", helper,
   311  		map[string]interface{}{
   312  			"origin": origin.URL,
   313  		})
   314  	helper.Must(cerr)
   315  	defer shutdown()
   316  
   317  	client := test.NewHTTPClient()
   318  
   319  	type testcase struct {
   320  		path      string
   321  		expStatus int
   322  	}
   323  
   324  	for i, tc := range []testcase{
   325  		{"/anon", http.StatusNoContent},
   326  		{"/ref", http.StatusNoContent},
   327  		{"/catch", http.StatusNoContent},
   328  		// server switched resp status-code -> unhealthy
   329  		{"/anon", http.StatusConflict}, // always healthy
   330  		{"/ref", http.StatusBadGateway},
   331  		{"/catch", http.StatusTeapot},
   332  	} {
   333  		t.Run(tc.path, func(st *testing.T) {
   334  			h := test.New(st)
   335  			req, err := http.NewRequest(http.MethodGet, "http://couper.dev:8080"+tc.path, nil)
   336  			h.Must(err)
   337  			req.Header.Set("Counter", strconv.Itoa(i))
   338  			res, err := client.Do(req)
   339  			h.Must(err)
   340  
   341  			if res.StatusCode != tc.expStatus {
   342  				st.Errorf("want status %d, got: %d", tc.expStatus, res.StatusCode)
   343  			}
   344  		})
   345  	}
   346  }
   347  
   348  func TestBackend_Oauth2_TokenEndpoint(t *testing.T) {
   349  	helper := test.New(t)
   350  
   351  	requestCount := 0
   352  	origin := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
   353  		rw.Header().Set("Content-Type", "application/json")
   354  		rw.WriteHeader(http.StatusUnauthorized)
   355  		_, werr := rw.Write([]byte(`{"path": "` + r.URL.Path + `"}`))
   356  		requestCount++
   357  		helper.Must(werr)
   358  	}))
   359  	defer origin.Close()
   360  
   361  	tokenEndpoint := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
   362  		rw.Header().Set("Content-Type", "application/json")
   363  		_, werr := rw.Write([]byte(`{
   364            	"access_token": "my-token",
   365            	"expires_in": 120
   366  		}`))
   367  		helper.Must(werr)
   368  	}))
   369  	defer tokenEndpoint.Close()
   370  
   371  	retries := 3
   372  	shutdown, _, cerr := newCouperWithTemplate("testdata/integration/backends/07_couper.hcl", helper,
   373  		map[string]interface{}{
   374  			"origin":         origin.URL,
   375  			"token_endpoint": tokenEndpoint.URL,
   376  			"retries":        retries,
   377  		})
   378  	helper.Must(cerr)
   379  	defer shutdown()
   380  
   381  	client := test.NewHTTPClient()
   382  
   383  	req, err := http.NewRequest(http.MethodGet, "http://couper.dev:8080/test-path", nil)
   384  	helper.Must(err)
   385  	res, err := client.Do(req)
   386  	helper.Must(err)
   387  
   388  	if res.StatusCode != http.StatusUnauthorized {
   389  		t.Errorf("want status %d, got: %d", http.StatusUnauthorized, res.StatusCode)
   390  	}
   391  
   392  	if res.Header.Get("Content-Type") != "application/json" {
   393  		t.Errorf("want json content-type")
   394  		return
   395  	}
   396  
   397  	type result struct {
   398  		Path string
   399  	}
   400  
   401  	b, err := io.ReadAll(res.Body)
   402  	helper.Must(err)
   403  	helper.Must(res.Body.Close())
   404  
   405  	r := &result{}
   406  	helper.Must(json.Unmarshal(b, r))
   407  
   408  	if r.Path != "/test-path" {
   409  		t.Errorf("path property want: %q, got: %q", "/test-path", r.Path)
   410  	}
   411  
   412  	if requestCount != retries+1 {
   413  		t.Errorf("unexpected number of requests, want: %d, got: %d", retries+1, requestCount)
   414  	}
   415  }
   416  
   417  func TestBackend_BackendVar(t *testing.T) {
   418  	helper := test.New(t)
   419  	shutdown, hook := newCouper("testdata/integration/backends/08_couper.hcl", helper)
   420  	defer shutdown()
   421  
   422  	client := test.NewHTTPClient()
   423  
   424  	hook.Reset()
   425  
   426  	req, _ := http.NewRequest(http.MethodGet, "http://couper.dev:8080/anything", nil)
   427  	res, err := client.Do(req)
   428  	helper.Must(err)
   429  
   430  	hHealthy1 := res.Header.Get("x-healthy-1")
   431  	hHealthy2 := res.Header.Get("x-healthy-2")
   432  	if hHealthy1 != "true" {
   433  		t.Errorf("expected x-healthy-1 to be true, got %q", hHealthy1)
   434  	}
   435  	if hHealthy2 != "true" {
   436  		t.Errorf("expected x-healthy-2 to be true, got %q", hHealthy2)
   437  	}
   438  	hRequestPath1 := res.Header.Get("x-rp-1")
   439  	hRequestPath2 := res.Header.Get("x-rp-2")
   440  	if hRequestPath1 != "/anything" {
   441  		t.Errorf("expected x-rp-1 to be %q, got %q", "/anything", hRequestPath1)
   442  	}
   443  	if hRequestPath2 != "/anything" {
   444  		t.Errorf("expected x-rp-2 to be %q, got %q", "/anything", hRequestPath2)
   445  	}
   446  	hResponseStatus1 := res.Header.Get("x-rs-1")
   447  	hResponseStatus2 := res.Header.Get("x-rs-2")
   448  	if hResponseStatus1 != "200" {
   449  		t.Errorf("expected x-rs-1 to be %q, got %q", "/200", hResponseStatus1)
   450  	}
   451  	if hResponseStatus2 != "200" {
   452  		t.Errorf("expected x-rs-2 to be %q, got %q", "/200", hResponseStatus2)
   453  	}
   454  
   455  	for _, e := range hook.AllEntries() {
   456  		if e.Data["type"] != "couper_backend" {
   457  			continue
   458  		}
   459  		custom, _ := e.Data["custom"].(logrus.Fields)
   460  
   461  		if lHealthy1, ok := custom["healthy_1"].(bool); !ok {
   462  			t.Error("expected healthy_1 to be set and bool")
   463  		} else if lHealthy1 != true {
   464  			t.Errorf("expected healthy_1 to be true, got %v", lHealthy1)
   465  		}
   466  		if lHealthy2, ok := custom["healthy_2"].(bool); !ok {
   467  			t.Error("expected healthy_2 to be set and bool")
   468  		} else if lHealthy2 != true {
   469  			t.Errorf("expected healthy_2 to be true, got %v", lHealthy2)
   470  		}
   471  
   472  		if lRequestPath1, ok := custom["rp_1"].(string); !ok {
   473  			t.Error("expected rp_1 to be set and string")
   474  		} else if lRequestPath1 != "/anything" {
   475  			t.Errorf("expected rp_1 to be %q, got %v", "/anything", lRequestPath1)
   476  		}
   477  		if lRequestPath2, ok := custom["rp_2"].(string); !ok {
   478  			t.Error("expected rp_2 to be set and string")
   479  		} else if lRequestPath2 != "/anything" {
   480  			t.Errorf("expected rp_2 to be %q, got %v", "/anything", lRequestPath2)
   481  		}
   482  
   483  		if lResponseStatus1, ok := custom["rs_1"].(float64); !ok {
   484  			t.Error("expected rs_1 to be set and float64")
   485  		} else if lResponseStatus1 != 200 {
   486  			t.Errorf("expected rs_1 to be %d, got %v", 200, lResponseStatus1)
   487  		}
   488  		if lResponseStatus2, ok := custom["rs_2"].(float64); !ok {
   489  			t.Error("expected rs_2 to be set and float64")
   490  		} else if lResponseStatus2 != 200 {
   491  			t.Errorf("expected rs_2 to be %d, got %v", 200, lResponseStatus2)
   492  		}
   493  	}
   494  }